SLOPSHOPPER

brief-guard

Replaces Claude Code prompt blocks that conflict with the Brief output style

newguardprompt
A shopper browsing a rack in a slop shop
README

Claude Code Agents and Skills

English · Українська · Русский

A portable snapshot of the author's local Claude Code setup, synchronized on 2026-10-15. It contains 18 engineering skills, 14 specialist agents, the Brief output style, and 2 mods, plus portable settings.

Skills cover clean and secure code, testing, documentation, architecture, design patterns, implementation plans, HTTP APIs, databases, interfaces, 3D, observability, resilient services, dependencies and CI, releases, commit messages, and LLM security. Agents provide codebase exploration, planning, web research, diagnosis, implementation, review, security audits, database analysis, documentation, test runs, and verification. The Brief output style keeps every final answer to one short, plain paragraph.

The two mods are Claude Code plugins with a hooks module:

  • brief-guard replaces the Claude Code prompt blocks that conflict with the Brief output style. It swaps system prompt sections and reminders for the texts in its instructions folder, adds the author's standing rules, extends tool descriptions, carries the rules through compaction, and adds reminders after tool errors, agent results, shell commands that read or write local files, and changes made before a skill's reference files or any web source were read. In bypassPermissions mode it also allows every permission request that still reaches you, except AskUserQuestion, EnterPlanMode, and ExitPlanMode.
  • usage-line shows plan limits, cost, and token totals above the prompt: the 5-hour and 7-day limits as gauges that change color as they fill, the session cost in US dollars, and the input, output, cache read, and cache write tokens.

This is a personal configuration, not an official Anthropic distribution. Credentials, account state, chat history, projects, sessions, plugins, synced skills, caches, and machine paths are not included.

[!WARNING] When your settings don't set them, the installer adds "permissions": { "defaultMode": "bypassPermissions" } and "skipDangerousModePermissionPrompt": true. Claude Code then runs every tool call without asking and skips the confirmation dialog for that mode, and brief-guard allows the remaining permission requests. The permission modes documentation recommends this mode only for isolated environments, such as containers and virtual machines without internet access. Review the settings before installing, or set your own permissions.defaultMode first, because the installer keeps existing values.

Install or replace an older setup

Download and extract the ZIP archive, or clone the repository:

git clone https://github.com/dayfinggg/anthropic-claude-code-agent-skills.git
cd anthropic-claude-code-agent-skills

Run one installer from the extracted or cloned folder. It uses the included files without downloading another copy.

Windows

Requires Windows PowerShell 5.1 or PowerShell 7.

powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install.ps1

macOS and Linux

Requires a POSIX shell, python3 for the settings merge, and a SHA-256 utility, such as sha256sum, shasum, or openssl.

sh scripts/install.sh

Restart Claude Code after installation. The mods load through CLAUDE_CODE_PLUGIN_DIRS, which requires Claude Code v2.1.280 or later.

Replace is the default. The installer backs up and replaces all skills except skills/synced, all agents, the output-styles folder, and the mods folder, removing the old set. It keeps synced skills, credentials, sessions, projects, plugins, and unrelated files. Existing settings values are preserved and missing keys are added, except outputStyle, which becomes Brief, and env.CLAUDE_CODE_PLUGIN_DIRS, which lists the installed mod folders.

Update

To download and install the latest GitHub revision without updating your checkout:

powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\update.ps1
sh scripts/update.sh

Updates require internet access, with curl and tar additionally required for the shell updater. The last install mode is reused. If you maintain a Git clone instead, run git pull --ff-only and rerun the installer.

Contents and installation details

The payload files are copied byte for byte from the local setup. The settings copy leaves out env.CLAUDE_CODE_PLUGIN_DIRS, which the installer writes for each platform, and each mod's .claude-plugin/types folder is left out because Claude Code generates those type definitions for the version it runs.

Claude Code keeps its configuration in ~/.claude, or in the folder named by CLAUDE_CONFIG_DIR, as described in Environment variables and Explore the .claude directory. Personal subagents load from its agents folder (Create custom subagents), skills from skills/<skill-name>/SKILL.md (Extend Claude with skills), and output styles from output-styles (Output styles). CLAUDE_CODE_PLUGIN_DIRS loads each listed folder the way --plugin-dir does, with absolute paths separated by ; on Windows and : elsewhere (Mods reference).

Verify the installers

Requires Python 3.11 or newer:

python -m unittest discover -s tests -v

Tests use temporary directories, not your real Claude Code setup. Available Windows PowerShell, PowerShell 7, and POSIX shell implementations are tested; unavailable runtimes are reported as skipped.

Report problems through GitHub issues. Repository instructions and scripts use the MIT License.

Source 2 files
hooks/register.ts 274 lines
1import type { EngineInterface, PromptComposeSection, Register } from 'claude-code'
2import type { TurnMarks } from '../types'
3
4const DROPPED_SECTIONS = ['tone']
5const DROPPED_REMINDERS = ['auto_mode', 'silent_turn_reminder', 'task_reminder', 'todo_reminder']
6const MCP_INSTRUCTIONS = 'mcp_instructions_delta'
7const STATUS_LINE_REQUEST = /they['’]re likely watching it fill[^)]*\);\s*/g
8
9const PERSON_TOOLS = ['AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode']
10const REQUEST_EXCERPT_LENGTH = 160
11const USER_ORIGINS: string[] = ['composer', 'bridge', 'sdk', 'unclassified']
12const NOTIFICATION_ORIGIN = 'task-notification'
13const EMPTY_REPLY_NUDGE = '[Your previous response had no visible output'
14
15const TURN = { plugin: 'brief-guard', key: 'turn' } as const
16const FRESH_TURN: TurnMarks = { request: '', silenceAllowed: false }
17
18const readTurn = async ($: EngineInterface): Promise<TurnMarks> => ({ ...FRESH_TURN, ...(await $.state.get(TURN)).value })
19
20const markTurn = async ($: EngineInterface, marks: Partial<TurnMarks>) => {
21  const current = await readTurn($)
22  const next = { ...current, ...marks }
23  if (Object.entries(marks).some(([key, value]) => current[key as keyof TurnMarks] !== value)) await $.state.set(TURN, next)
24}
25
26const nonEmptyLines = (text: string) => text.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
27
28const typedTextOf = (text: string) =>
29  text
30    .replace(/<!--[\s\S]*?-->/g, ' ')
31    .replace(/<([A-Za-z][\w-]*)\b[^>]*>[\s\S]*?<\/\1\b[^>]*>/g, ' ')
32    .split(/\r?\n/)
33    .filter(line => !line.trimStart().startsWith('>'))
34    .join('\n')
35
36const excerptOf = (text: string) =>
37  typedTextOf(text)
38    .replace(/```[\s\S]*?```/g, ' ')
39    .replace(/\s+/g, ' ')
40    .trim()
41    .slice(0, REQUEST_EXCERPT_LENGTH)
42
43const SHELL_TOOLS = ['Bash', 'PowerShell']
44const FILE_READERS = ['cat', 'head', 'tail', 'less', 'more', 'Get-Content', 'gc', 'type']
45const FILE_WRITERS = ['Set-Content', 'Add-Content', 'Out-File', 'tee']
46const SCRIPT_RUNNERS = /^(python3?|node|perl|ruby|php)$/
47
48const hasOperand = (words: string[]) => words.slice(1).some(w => !w.startsWith('-') && !/^\d+$/.test(w))
49
50const programOf = (stage: string): string | undefined => {
51  const words = stage.trim().split(/\s+/).filter(w => !/^\w+=/.test(w))
52  const [name = ''] = words
53  if (FILE_READERS.includes(name) && hasOperand(words)) return name
54  if (FILE_WRITERS.includes(name)) return name
55  if (name === 'sed' && words.some(w => /^-[a-zA-Z]*[ni]/.test(w))) return 'sed'
56  if (name === 'awk' && words.length > 2) return 'awk'
57  if (SCRIPT_RUNNERS.test(name) && words.slice(1).some(w => w === '-' || /^-[a-zA-Z]*[ceE]$/.test(w) || w.startsWith('<<'))) return `inline ${name}`
58  return undefined
59}
60
61const localFileProgram = (command: string): string | undefined => {
62  if (/\b(ssh|scp|docker|kubectl)\b/.test(command)) return undefined
63  const body = command.replace(/<<-?\s*'?(\w+)'?[\s\S]*?\n\1\b/g, '<<heredoc')
64  for (const part of body.split(/&&|\|\||;|\n/)) {
65    const [first = '', ...piped] = part.split('|')
66    const found = programOf(first.replace(/^\s*(cd\s+\S+|\(|\{)\s*/, '')) ?? piped.map(stage => programOf(stage)).find(p => p === 'tee' || p === 'Out-File' || p === 'Set-Content' || p === 'Add-Content')
67    if (found) return found
68  }
69  return undefined
70}
71
72const fill = (template: string, values: Record<string, string>) =>
73  Object.entries(values).reduce((text, [key, value]) => text.split(`{${key}}`).join(value), template)
74
75const hasEmptyValue = (line: string, values: Record<string, string>) =>
76  Object.entries(values).some(([key, value]) => !value && line.includes(`{${key}}`))
77
78const fillLines = (template: string, values: Record<string, string>) =>
79  nonEmptyLines(template)
80    .filter(line => !hasEmptyValue(line, values))
81    .map(line => fill(line, values))
82    .join('\n')
83
84const CHANGE_TOOLS = ['Edit', 'Write', 'NotebookEdit']
85const SKILL_TOOL = 'Skill'
86const WEB_TOOLS = /^(WebSearch|WebFetch)$|^mcp__.*(search|fetch|docs)/i
87const REFERENCE_DIRS = ['reference', 'references']
88const SKILL_OWN_FILES = /^(skill|license|readme)\b/i
89
90type SkillReferences = { files: string[]; read: Set<string>; reminded: boolean }
91type LoopState = {
92  webRead: boolean
93  webReminded: boolean
94  skillReferences: Map<string, SkillReferences>
95}
96
97const mainLoop: LoopState = {
98  webRead: false,
99  webReminded: false,
100  skillReferences: new Map(),
101}
102let skillCallerAgentId: string | undefined
103
104const isLaunchOnly = (output: unknown) =>
105  typeof output === 'object' && output !== null && 'status' in output && String(output.status).endsWith('_launched')
106
107const matchesAny =(patterns: string[], tool: string) =>
108  patterns.some(pattern => (pattern.endsWith('*') ? tool.startsWith(pattern.slice(0, -1)) : tool === pattern))
109
110const samePath = (a: string, b: string) => a.replace(/\\/g, '/').toLowerCase() === b.replace(/\\/g, '/').toLowerCase()
111
112const skillBaseOf = (text: string) => /Base directory for this skill: (.+)/.exec(text)?.[1]?.trim()
113
114const referenceFilesOf = async ($: EngineInterface, base: string) => {
115  const separator = base.includes('\\') ? '\\' : '/'
116  const join = (...parts: string[]) => parts.join(separator)
117  const markdownIn = async (dir: string, keep: (name: string) => boolean) =>
118    (await $.fs.exists(dir)) ? (await $.fs.list(dir)).filter(f => f.kind === 'file' && /\.md$/i.test(f.name) && keep(f.name)).map(f => join(dir, f.name)) : []
119  const files: string[] = []
120  for (const dir of REFERENCE_DIRS) files.push(...(await markdownIn(join(base, dir), () => true)))
121  files.push(...(await markdownIn(base, name => !SKILL_OWN_FILES.test(name))))
122  return files
123}
124
125const markReferenceRead = (skillReferences: Map<string, SkillReferences>, path: string) => {
126  for (const skill of skillReferences.values()) for (const file of skill.files) if (samePath(file, path)) skill.read.add(file)
127}
128
129const takeUnremindedSkills = (skillReferences: Map<string, SkillReferences>) => {
130  const unread = [...skillReferences].filter(([, skill]) => !skill.reminded && !skill.read.size && skill.files.length)
131  for (const [, skill] of unread) skill.reminded = true
132  return unread.length ? { names: unread.map(([name]) => name).join(' and '), files: unread.flatMap(([, skill]) => skill.files) } : undefined
133}
134
135export const register: Register = on => {
136  on('prompt.compose', async ($, e, next) => {
137    let sections: PromptComposeSection[] = (await next(e)).sections.filter(s => !DROPPED_SECTIONS.includes(s.id))
138    const dir = `${$.plugin.root}/instructions`
139    const mdIn = async (path: string) =>
140      (await $.fs.list(path)).filter(f => f.kind === 'file' && f.name.endsWith('.md')).map(f => f.name).sort()
141
142    for (const name of await mdIn(dir)) {
143      const ids = name.slice(0, -3).split('+')
144      const text = (await $.fs.read(`${dir}/${name}`)).trim()
145      sections = sections.map(s => (ids.includes(s.id) ? { ...s, text } : s))
146    }
147
148    for (const name of await mdIn(`${dir}/added`)) {
149      const text = (await $.fs.read(`${dir}/added/${name}`)).trim()
150      sections.push({ id: `brief-guard:${name.slice(0, -3)}`, text, scope: 'session' })
151    }
152
153    return { sections }
154  })
155
156  on('prompt.attachment', async ($, e, next) => {
157    if (DROPPED_REMINDERS.includes(e.type)) return { text: null }
158    if (e.type === MCP_INSTRUCTIONS) return next({ ...e, text: e.text.replace(STATUS_LINE_REQUEST, '') })
159    const path = `${$.plugin.root}/instructions/reminders/${e.type}.md`
160    const { request } = await readTurn($)
161    return (await $.fs.exists(path)) ? { text: fillLines(await $.fs.read(path), { request }) } : next(e)
162  })
163
164  on('skill.prompt', async ($, e, next) => {
165    const { text } = await next(e)
166    if (!skillCallerAgentId) {
167      const base = skillBaseOf(text)
168      const files = base ? await referenceFilesOf($, base).catch(() => []) : []
169      mainLoop.skillReferences.set(e.skill, { files, read: new Set(), reminded: false })
170    }
171    const footer = (await $.fs.read(`${$.plugin.root}/instructions/skills/footer.md`).catch(() => '')).trim()
172    return { text: footer ? `${text}\n\n${footer}` : text }
173  })
174
175  on('prompt.submit', async ($, e, next) => {
176    const request = USER_ORIGINS.includes(e.origin.kind) ? excerptOf(e.text) : ''
177    if (request) await markTurn($, { request })
178    if (e.origin.kind !== NOTIFICATION_ORIGIN) return next(e)
179    const template = await $.fs.read(`${$.plugin.root}/instructions/reminders/task_notification.md`).catch(() => '')
180    const note = fillLines(template, { request: (await readTurn($)).request })
181    return next(note ? { ...e, context: [...(e.context ?? []), note] } : e)
182  })
183
184  on('turn.start', async ($, e, next) => {
185    await markTurn($, { silenceAllowed: false })
186    mainLoop.webRead = false
187    mainLoop.webReminded = false
188    return next(e)
189  })
190
191
192  on('tool.call', async ($, e, next) => {
193    if (e.tool === SKILL_TOOL) skillCallerAgentId = e.agentId
194    const result = await next(e)
195    if (e.agentId || result.deny !== undefined) return result
196    const command = 'command' in e && typeof e.command === 'string' ? e.command : ''
197    const filePath = 'file_path' in e && typeof e.file_path === 'string' ? e.file_path : ''
198    if (e.tool === 'Read' && filePath) markReferenceRead(mainLoop.skillReferences, filePath)
199    if (WEB_TOOLS.test(String(e.tool))) mainLoop.webRead = true
200    const isChange = CHANGE_TOOLS.includes(String(e.tool))
201    const shellProgram = SHELL_TOOLS.includes(String(e.tool)) ? localFileProgram(command) : undefined
202    const unreadSkill = isChange ? takeUnremindedSkills(mainLoop.skillReferences) : undefined
203    const needsWeb = isChange && !mainLoop.webRead && !mainLoop.webReminded
204    if (needsWeb) mainLoop.webReminded = true
205    const names = [
206      result.isError && 'tool_error',
207      e.tool === 'Agent' && (isLaunchOnly(result.result) ? 'agent_launched' : 'agent_result'),
208      shellProgram && 'shell_files',
209      unreadSkill && 'skill_refs',
210      needsWeb && 'web_sources',
211    ].filter((name): name is string => Boolean(name))
212    if (!names.length) return result
213    const templates = await Promise.all(
214      names.map(name => $.fs.read(`${$.plugin.root}/instructions/reminders/${name}.md`).catch(() => '')),
215    )
216    const values = {
217      request: (await readTurn($)).request,
218      program: shellProgram ?? '',
219      skill: unreadSkill?.names ?? '',
220      files: unreadSkill?.files.join('\n') ?? '',
221    }
222    const text = fillLines(templates.join('\n'), values)
223    return text ? { ...result, context: [...(result.context ?? []), text] } : result
224  }).catch(($, e, next) => next(e))
225
226  on('classic.PermissionRequest', async ($, e, next) => {
227    const result = await next(e)
228    const isBypassed = e.permission_mode === 'bypassPermissions' && !PERSON_TOOLS.includes(e.tool_name)
229    return isBypassed && !result.decision ? { ...result, decision: { behavior: 'allow' } } : result
230  })
231
232  on('session.compact', async ($, e, next) => {
233    const extra = (await $.fs.read(`${$.plugin.root}/instructions/compact/summary.md`).catch(() => '')).trim()
234    const instructions = [e.instructions, extra].filter(Boolean).join('\n\n')
235    return next(instructions ? { ...e, instructions } : e)
236  }).catch(($, e, next) => next(e))
237
238  on('session.append', { door: 'compaction' }, async ($, e, next) => {
239    if (e.agentId || e.message.type !== 'user') return next(e)
240    const note = (await $.fs.read(`${$.plugin.root}/instructions/compact/after.md`).catch(() => '')).trim()
241    if (!note) return next(e)
242    const content = typeof e.message.content === 'string' ? [{ type: 'text' as const, text: e.message.content }] : e.message.content
243    return next({ ...e, message: { ...e.message, content: [...content, { type: 'text', text: note }] } })
244  }).catch(($, e, next) => next(e))
245
246  on('session.append', async ($, e, next) => {
247    if (e.agentId || e.message.type !== 'user') return next(e)
248    const content = typeof e.message.content === 'string' ? [{ type: 'text' as const, text: e.message.content }] : e.message.content
249    const isEmptyReplyNudge = content.some(block => block.type === 'text' && typeof block.text === 'string' && block.text.startsWith(EMPTY_REPLY_NUDGE))
250    if (!isEmptyReplyNudge) return next(e)
251    const turn = await readTurn($)
252    const name = turn.silenceAllowed ? 'empty_reply_again' : 'empty_reply'
253    const template = await $.fs.read(`${$.plugin.root}/instructions/reminders/${name}.md`).catch(() => '')
254    const note = fillLines(template, { request: turn.request })
255    if (!note) return next(e)
256    if (!turn.silenceAllowed) await markTurn($, { silenceAllowed: true })
257    const kept = turn.silenceAllowed ? content : []
258    return next({ ...e, message: { ...e.message, content: [...kept, { type: 'text', text: note }] } })
259  }).catch(($, e, next) => next(e))
260
261  on('tool.describe', async ($, e, next) => {
262    const dir = `${$.plugin.root}/instructions/tools`
263    const readList = async (name: string) => nonEmptyLines(await $.fs.read(`${dir}/${name}`).catch(() => ''))
264    const readNote = async (name: string) => (await $.fs.read(`${dir}/${name}`).catch(() => '')).trim()
265    const isDeferred = matchesAny(await readList('deferred.md'), e.tool)
266    const ownGuidance = /^[\w-]+$/.test(e.tool) ? await readNote(`${e.tool}.md`) : ''
267    const callNote = matchesAny(await readList('silent.txt'), e.tool) ? await readNote('silent.md') : ''
268    const guidance = [ownGuidance, callNote].filter(Boolean).join('\n\n')
269    const result = await next(e)
270    const described = guidance ? { ...result, description: `${result.description}\n\n${guidance}` } : result
271    return isDeferred ? { ...described, isDeferred: true } : described
272  })
273}
274
types/index.d.ts 11 lines
1export type TurnMarks = {
2  request: string
3  silenceAllowed: boolean
4}
5
6declare module 'claude-code' {
7  interface PluginState {
8    'brief-guard': { turn: TurnMarks }
9  }
10}
11