SLOPSHOPPER

guard

Blocks destructive shell commands and access to secret files before they run, anywhere in a compound command

newguardcommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · guard
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(rm -rf build && git push --force origin main) ⎿ Denied by guard: guard blocked this (git-force-push): it force-pushes and can overwrite commits on the rem ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /guard ⎿ guard: Active. ⎿ guard: destructive commands: rm -rf (dangerous targets), git push --force, git reset --hard, git clean -f, chmod -R ⎿ guard: secret files: .env, .env.*, *.pem, *.key, *.p12, *.pfx, id_rsa, id_dsa, id_ecdsa, id_ed25519, .npmrc, .pypir ⎿ guard: Blocked this session (2): ⎿ guard: Bash · git-force-push · git push --force origin main ⎿ guard: Bash · secret-file · cat .env ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ guard: guard: 2 blocked
README

guard

Blocks destructive shell commands and access to secret files before they run.

Settings permissions.deny rules match command prefixes, so cd app && rm -rf ~, rm -fr /, rm -r -f / or git push origin main -f get past them. guard sees every tool call inside Claude Code, splits compound commands, normalizes flags and wrappers (sudo, env, VAR=1), and resolves symlinks before it lets a file through.

/plugin install guard@claude-mods

What it blocks

Destructive commands (Bash), anywhere in a compound command:

RuleBlocksLets through
rm-rfrm recursive + force on /, ~, $HOME, ., .., *, /usr, /home/me, C:/, a bare $VAR/rm -rf node_modules, rm -rf ./dist, rm -rf "${OUT:?}/"
git-force-push--force, -f, -fu, +refspec--force-with-lease
git-reset-hardgit reset --hard--soft, --mixed
git-cleangit clean -f…git clean -n
chmod-777chmod -R 777chmod 755 file
disk-writemkfs*, dd of=/dev/…dd of=./disk.img
pipe-to-shell`curl … \sh, wget … \sudo bash`curl -o install.sh …
fork-bomb`:(){ :\:& };:`

Secret files: .env, .env.*, *.pem, *.key, *.p12, *.pfx, id_rsa, id_ed25519 and friends, .npmrc, .pypirc, .netrc, .git-credentials, .aws/credentials, .docker/config.json, except .env.example, .env.sample, .env.template, .env.dist and *.pub.

They are blocked for Read, Edit, Write, NotebookEdit and Grep, on the path as given and on where it resolves (a symlink to .env is .env), and in shell commands that read, send or write them: cat .env, grep KEY .env, curl -d @.env, echo X >> .env, cp .env /tmp, git add .env. Commands that only name them pass: cp .env.example .env, echo .env >> .gitignore, source .env.

The model gets the rule and what to do instead, e.g.:

guard blocked this (git-force-push): it force-pushes and can overwrite commits on the remote. Use --force-with-lease, or ask the user.

/guard

  • /guard lists the active rules and what was blocked this session
  • /guard off pauses it for the session, /guard on resumes it. Only accepted from the person (the prompt, Remote Control, the SDK host), never from another agent, a peer session or a channel message

The status line shows guard: N blocked (or guard: paused).

Options

Set them in /config or under pluginConfigs.guard.options in settings:

OptionDefault
destructivetrueBlock destructive commands
rmModedangerousany blocks every rm -rf
secretstrueProtect secret files
blockCommands[]Extra regular expressions tested against every Bash command, e.g. \bnpm publish\b
protectPaths[]Extra gitignore-like globs no tool may read or write, e.g. migrations/**
allowPaths[]Globs exempt from the file rules, e.g. test/fixtures/**

Limits

guard is a safety net against accidents, not a sandbox. The command check is a heuristic, not a shell parser: eval, scripts the model writes and then runs, interpreters (python -c, node -e) and the PowerShell tool are not inspected. Use Claude Code's sandbox and permission modes for containment.

Source 3 files
hooks/register.ts 131 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { GuardBlock } from '../types'
5import { buildPolicy, checkCommand, checkPath, denyMessage, describePolicy } from './rules'
6import type { Policy, Verdict } from './rules'
7
8const isPaused = atom({ plugin: 'guard', key: 'isPaused' } as const, false)
9const blocks = atom({ plugin: 'guard', key: 'blocks' } as const, [])
10
11/** Who may pause the guard: the person, never another agent, a channel or a peer session. */
12const PERSON = new Set(['composer', 'bridge', 'sdk'])
13
14const MAX_BLOCKS = 50
15
16async function showStatus($: EngineInterface) {
17  const count = (await read($, blocks)).length
18  const paused = await read($, isPaused)
19  $.ui.status(paused ? 'guard: paused' : count > 0 ? `guard: ${count} blocked` : undefined)
20}
21
22async function deny($: EngineInterface, tool: string, verdict: Verdict) {
23  const block: GuardBlock = { tool, rule: verdict.rule, subject: verdict.subject, at: await $.clock.now() }
24  await update($, blocks, list => [...list, block].slice(-MAX_BLOCKS))
25  await showStatus($)
26
27  return { deny: denyMessage(verdict) }
28}
29
30// The path as given, then where it really leads: a symlink to a secret file is
31// a secret file. A path that does not exist yet has no realPath.
32async function checkFile($: EngineInterface, path: string | undefined, policy: Policy) {
33  if (path === undefined || path === '') return undefined
34  const spelled = checkPath(path, policy)
35  if (spelled) return spelled
36
37  const stat = await $.fs.stat(path, { resolve: true }).catch(() => undefined)
38  const real = stat?.realPath
39  const resolved = real !== undefined && real !== path ? checkPath(real, policy) : undefined
40
41  return resolved ? { ...resolved, subject: `${path} → ${real}` } : undefined
42}
43
44export const register: Register = (on, options) => {
45  const { policy, problems } = buildPolicy(options)
46
47  on('session.start', async ($, e, next) => {
48    await $.command.register({
49      name: 'guard',
50      description: 'Show the guard rules and what it blocked; /guard off or /guard on pauses it for this session',
51      argumentHint: '[on|off]',
52    })
53    if (problems.length > 0) $.ui.toast(`guard: ${problems.join('; ')}`)
54
55    return next(e)
56  })
57
58  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
59    if (await read($, isPaused)) return next(e)
60    const verdict = checkCommand(e.command, policy)
61
62    return verdict ? deny($, e.tool, verdict) : next(e)
63  })
64
65  on('tool.call', { tool: 'Read' }, async ($, e, next) => {
66    if (await read($, isPaused)) return next(e)
67    const verdict = await checkFile($, e.file_path, policy)
68
69    return verdict ? deny($, e.tool, verdict) : next(e)
70  })
71
72  on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
73    if (await read($, isPaused)) return next(e)
74    const verdict = await checkFile($, e.file_path, policy)
75
76    return verdict ? deny($, e.tool, verdict) : next(e)
77  })
78
79  on('tool.call', { tool: 'Write' }, async ($, e, next) => {
80    if (await read($, isPaused)) return next(e)
81    const verdict = await checkFile($, e.file_path, policy)
82
83    return verdict ? deny($, e.tool, verdict) : next(e)
84  })
85
86  on('tool.call', { tool: 'NotebookEdit' }, async ($, e, next) => {
87    if (await read($, isPaused)) return next(e)
88    const verdict = await checkFile($, e.notebook_path, policy)
89
90    return verdict ? deny($, e.tool, verdict) : next(e)
91  })
92
93  // Grep is a built-in tool on some builds only (others search through Bash),
94  // so it is matched by name rather than by this build's tool types.
95  on('tool.call', async ($, e, next) => {
96    if (String(e.tool) !== 'Grep' || (await read($, isPaused))) return next(e)
97    const { path } = e as { path?: unknown }
98    const verdict = typeof path === 'string' ? await checkFile($, path, policy) : undefined
99
100    return verdict ? deny($, e.tool, verdict) : next(e)
101  })
102
103  on('command.run', { command: 'guard' }, async ($, e) => {
104    const arg = e.args.trim().toLowerCase()
105
106    if (arg === 'off' || arg === 'on') {
107      if (!PERSON.has(e.origin.kind)) {
108        return { text: `/guard ${arg} is only accepted from the person at the prompt (got ${e.origin.kind}).` }
109      }
110      await update($, isPaused, () => arg === 'off')
111      await showStatus($)
112
113      return {
114        text: arg === 'off' ? 'Paused for this session. /guard on resumes it.' : 'Active.',
115      }
116    }
117    if (arg !== '' && arg !== 'status') return { text: 'Usage: /guard [on|off]' }
118
119    const list = await read($, blocks)
120    const paused = await read($, isPaused)
121    const lines = [
122      paused ? 'Paused for this session (/guard on resumes it).' : 'Active.',
123      ...describePolicy(policy).map(line => `  ${line}`),
124      list.length === 0 ? 'Nothing blocked this session.' : `Blocked this session (${list.length}):`,
125      ...list.slice(-10).map(block => `  ${block.tool} · ${block.rule} · ${block.subject.slice(0, 80)}`),
126    ]
127
128    return { text: lines.join('\n') }
129  })
130}
131
hooks/rules.ts 389 lines
1// The guard's rules as pure functions: no `$`, so tests call them directly.
2
3export type Verdict = {
4  /** The rule that fired, as /guard lists it. */
5  rule: string
6  /** What was matched: a command segment or a path. */
7  subject: string
8  /** Why it is blocked, for the model and the person. */
9  reason: string
10  /** What to do instead. */
11  hint: string
12}
13
14export type RmMode = 'dangerous' | 'any'
15
16export type Policy = {
17  destructive: boolean
18  rmMode: RmMode
19  commands: { source: string; pattern: RegExp }[]
20  secrets: boolean
21  protect: { source: string; pattern: RegExp }[]
22  allow: RegExp[]
23}
24
25export const SECRET_GLOBS = [
26  '.env',
27  '.env.*',
28  '*.pem',
29  '*.key',
30  '*.p12',
31  '*.pfx',
32  'id_rsa',
33  'id_dsa',
34  'id_ecdsa',
35  'id_ed25519',
36  '.npmrc',
37  '.pypirc',
38  '.netrc',
39  '.git-credentials',
40  '.aws/credentials',
41  '.docker/config.json',
42]
43
44export const SECRET_EXCEPTIONS = ['.env.example', '.env.sample', '.env.template', '.env.dist', '*.pub']
45
46const SECRETS = SECRET_GLOBS.map(glob => globToRegExp(glob))
47const EXCEPTIONS = SECRET_EXCEPTIONS.map(glob => globToRegExp(glob))
48
49/**
50 * A gitignore-like glob: `*` and `?` stay within one path segment, `**`
51 * crosses them, and a pattern matches the end of the path at a segment
52 * boundary, so `.env` matches `/repo/.env` and `.aws/credentials` matches
53 * `~/.aws/credentials`. Case-insensitive, since some file systems are.
54 */
55export function globToRegExp(glob: string): RegExp {
56  const g = glob.replace(/\\/g, '/').replace(/^\.\//, '')
57  let source = ''
58
59  for (let i = 0; i < g.length; i++) {
60    const char = g[i]!
61    if (char === '*' && g[i + 1] === '*') {
62      i++
63      if (g[i + 1] === '/') {
64        i++
65        source += '(?:.*/)?'
66      } else {
67        source += '.*'
68      }
69    } else if (char === '*') {
70      source += '[^/]*'
71    } else if (char === '?') {
72      source += '[^/]'
73    } else {
74      source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&')
75    }
76  }
77
78  return new RegExp(`(?:^|/)${source}$`, 'i')
79}
80
81const normalize = (path: string) => path.replace(/\\/g, '/')
82
83const asList = (value: unknown): string[] =>
84  Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string' && item.trim() !== '') : []
85
86/** Builds the policy from the mod's options; bad patterns are reported, not thrown. */
87export function buildPolicy(options: Readonly<Record<string, unknown>>): { policy: Policy; problems: string[] } {
88  const problems: string[] = []
89  const commands: Policy['commands'] = []
90
91  for (const source of asList(options.blockCommands)) {
92    try {
93      commands.push({ source, pattern: new RegExp(source) })
94    } catch {
95      problems.push(`invalid blockCommands pattern /${source}/`)
96    }
97  }
98
99  return {
100    policy: {
101      destructive: options.destructive !== false,
102      rmMode: options.rmMode === 'any' ? 'any' : 'dangerous',
103      commands,
104      secrets: options.secrets !== false,
105      protect: asList(options.protectPaths).map(source => ({ source, pattern: globToRegExp(source) })),
106      allow: asList(options.allowPaths).map(source => globToRegExp(source)),
107    },
108    problems,
109  }
110}
111
112/** Checks one path (as given, or resolved) against the secret files and the protected paths. */
113export function checkPath(path: string, policy: Policy): Verdict | undefined {
114  const p = normalize(path)
115  if (policy.allow.some(pattern => pattern.test(p))) return undefined
116
117  if (policy.secrets && SECRETS.some(pattern => pattern.test(p)) && !EXCEPTIONS.some(pattern => pattern.test(p))) {
118    return {
119      rule: 'secret-file',
120      subject: path,
121      reason: 'it is a secret file (credentials, keys, environment)',
122      hint: 'Work from an example file such as .env.example, or ask the user for the value you need.',
123    }
124  }
125
126  const protectedBy = policy.protect.find(({ pattern }) => pattern.test(p))
127  if (protectedBy) {
128    return {
129      rule: 'protected-path',
130      subject: path,
131      reason: `it matches the protected path ${protectedBy.source}`,
132      hint: 'Leave this file alone, or ask the user to change it.',
133    }
134  }
135
136  return undefined
137}
138
139// ---------------------------------------------------------------------------
140// Shell commands
141
142const PREFIXES = new Set(['sudo', 'doas', 'command', 'builtin', 'exec', 'nohup', 'time', 'nice', 'ionice', 'xargs'])
143const OPTION_WITH_VALUE = new Set(['-u', '-g', '-n', '-c', '-C', '-p', '-I', '-L', '-P'])
144
145/** Programs that read, copy, send or edit the files they are given. */
146const FILE_TOUCHERS = new Set([
147  'cat', 'tac', 'less', 'more', 'head', 'tail', 'bat', 'nl', 'xxd', 'od', 'hexdump', 'strings', 'base64',
148  'grep', 'egrep', 'fgrep', 'rg', 'ag', 'awk', 'sed', 'cut', 'sort', 'uniq', 'diff', 'jq', 'yq',
149  'cp', 'mv', 'ln', 'scp', 'rsync', 'tee', 'curl', 'wget', 'nc', 'zip', 'tar', 'gzip',
150  'nano', 'vi', 'vim', 'nvim', 'emacs', 'code', 'open', 'type', 'truncate', 'shred',
151])
152
153/** Programs whose last argument is a destination: writing a secret file from a template is fine. */
154const COPIERS = new Set(['cp', 'mv', 'ln', 'scp', 'rsync'])
155
156const unquote = (token: string) => token.replace(/^(['"])(.*)\1$/, '$2')
157
158/**
159 * Splits a command line into simple commands (on `;`, `&&`, `||`, `|`, `&`,
160 * newlines, `$(`, backticks and parentheses) and each into words. Quoting is
161 * honoured for grouping only: this is a heuristic, not a shell parser.
162 */
163export function splitCommand(command: string): string[][] {
164  return command
165    .split(/\|\||&&|[;&|\n()`]|\$\(/)
166    .map(part => (part.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map(unquote))
167    .filter(words => words.length > 0)
168}
169
170/** Drops wrappers (`sudo -u x`, `env A=1`, `VAR=1`, `nohup`) to reach the program and its arguments. */
171export function stripPrefixes(words: string[]): string[] {
172  const rest = [...words]
173
174  for (;;) {
175    const first = rest[0]
176    if (first === undefined) return rest
177    const name = basename(first)
178
179    if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first)) {
180      rest.shift()
181    } else if (name === 'env') {
182      rest.shift()
183      while (rest[0] !== undefined && (rest[0].startsWith('-') || rest[0].includes('='))) rest.shift()
184    } else if (PREFIXES.has(name)) {
185      rest.shift()
186      while (rest[0]?.startsWith('-')) {
187        const option = rest.shift()!
188        if (OPTION_WITH_VALUE.has(option)) rest.shift()
189      }
190    } else {
191      return rest
192    }
193  }
194}
195
196const basename = (word: string) => normalize(word).split('/').pop() ?? word
197
198const shortFlags = (args: string[]) => args.filter(arg => /^-[^-]/.test(arg)).join('')
199
200const hasFlag = (args: string[], short: string, long: string) =>
201  args.includes(long) || shortFlags(args).includes(short)
202
203/** A target an `rm -rf` must not reach: a root, a home, the whole project, an unset variable. */
204export function isDangerousTarget(target: string): boolean {
205  const t = normalize(target)
206  const p = t.length > 1 ? t.replace(/\/+$/, '') : t
207
208  return (
209    /^(\/|\/\*|~|~\/\*|\$\{?HOME\}?(\/\*)?)$/.test(p) ||
210    /^(\.|\.\/\*|\*|\.\*|\.\.|\.\.\/\*)$/.test(p) ||
211    /^\/[^/]+(\/[^/]+)?(\/\*)?$/.test(p) ||
212    /^(~|\$\{?HOME\}?)\/[^/]+$/.test(p) ||
213    /^[A-Za-z]:(\/[^/]*)?$/.test(p) ||
214    // `$DIR/` or `"$DIR"/*` with DIR unset is `/`; `${DIR:?}` refuses to expand empty
215    (/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?(\/\*?)?$/.test(p) && !p.includes(':?'))
216  )
217}
218
219const deny = (rule: string, words: string[], reason: string, hint: string): Verdict => ({
220  rule,
221  subject: words.join(' '),
222  reason,
223  hint,
224})
225
226function checkDestructive(words: string[], rmMode: RmMode): Verdict | undefined {
227  const [program = '', ...args] = words
228  const name = basename(program)
229
230  if (name === 'rm') {
231    const end = args.indexOf('--')
232    const flags = (end < 0 ? args : args.slice(0, end)).filter(arg => arg.startsWith('-'))
233    const targets = args.filter((arg, i) => !(arg.startsWith('-') && (end < 0 || i < end)) && arg !== '--')
234    const isRecursive = flags.includes('--recursive') || /[rR]/.test(shortFlags(flags))
235    const isForced = flags.includes('--force') || shortFlags(flags).includes('f')
236
237    if (isRecursive && isForced) {
238      if (rmMode === 'any') {
239        return deny('rm-rf', words, 'it force-deletes recursively', 'Delete specific files, or ask the user to run it.')
240      }
241      const target = targets.find(isDangerousTarget)
242      if (target !== undefined) {
243        return deny(
244          'rm-rf',
245          words,
246          `it force-deletes ${target} recursively`,
247          'Name a specific path inside the project, guard variables with ${VAR:?}, or ask the user to run it.',
248        )
249      }
250    }
251  }
252
253  if (name === 'git') {
254    let i = 0
255    while (i < args.length && args[i]!.startsWith('-')) i += args[i] === '-C' || args[i] === '-c' ? 2 : 1
256    const sub = args[i]
257    const rest = args.slice(i + 1)
258
259    if (sub === 'push') {
260      const isForced =
261        rest.includes('--force') || shortFlags(rest).includes('f') || rest.some(arg => /^\+[^+]/.test(arg))
262      if (isForced) {
263        return deny(
264          'git-force-push',
265          words,
266          'it force-pushes and can overwrite commits on the remote',
267          'Use --force-with-lease, or ask the user.',
268        )
269      }
270    }
271    if (sub === 'reset' && rest.includes('--hard')) {
272      return deny(
273        'git-reset-hard',
274        words,
275        'it discards uncommitted changes for good',
276        'Use git stash, or ask the user.',
277      )
278    }
279    if (sub === 'clean' && hasFlag(rest, 'f', '--force') && !hasFlag(rest, 'n', '--dry-run')) {
280      return deny(
281        'git-clean',
282        words,
283        'it deletes untracked files for good',
284        'Preview with git clean -n and ask the user.',
285      )
286    }
287  }
288
289  if (name === 'chmod' && hasFlag(args, 'R', '--recursive') && args.some(arg => /^(0?777|a\+rwx|ugo\+rwx)$/.test(arg))) {
290    return deny('chmod-777', words, 'it makes a whole tree world-writable', 'Grant the narrowest permission needed.')
291  }
292
293  if (name.startsWith('mkfs') || (name === 'dd' && args.some(arg => /^of=\/dev\//.test(arg)))) {
294    return deny('disk-write', words, 'it writes a file system or raw data to a device', 'Ask the user to run it.')
295  }
296
297  return undefined
298}
299
300function checkWhole(command: string): Verdict | undefined {
301  const piped = command.match(/\b(curl|wget)\b[^\n;&|]*\|\s*(sudo\s+)?(env\s+(\S+=\S*\s+)*)?(ba|z|da|k|fi)?sh\b/)
302  if (piped) {
303    return {
304      rule: 'pipe-to-shell',
305      subject: piped[0],
306      reason: 'it runs a downloaded script without review',
307      hint: 'Download the script to a file, show it to the user, and run it only after they agree.',
308    }
309  }
310  if (/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(command)) {
311    return { rule: 'fork-bomb', subject: ':(){ :|:& };:', reason: 'it is a fork bomb', hint: 'Do not run it.' }
312  }
313
314  return undefined
315}
316
317function checkSecretMentions(words: string[], policy: Policy): Verdict | undefined {
318  const [program = '', ...args] = words
319  const name = basename(program)
320  const isGitAdd = name === 'git' && args.includes('add')
321  const destination = COPIERS.has(name) ? args.findLastIndex(arg => !arg.startsWith('-')) : -1
322
323  for (let i = 0; i < args.length; i++) {
324    if (i === destination) continue
325    const arg = args[i]!
326    const isRedirect = /^\d*(>>?|<)&?$/.test(arg)
327    const target = isRedirect ? args[i + 1] : arg.replace(/^\d*(>>?|<)&?/, '')
328
329    if (!isRedirect && !/^\d*(>>?|<)/.test(arg) && !FILE_TOUCHERS.has(name) && !isGitAdd) continue
330    if (target === undefined || target === '' || target.startsWith('-')) continue
331
332    const verdict = checkPath(target.replace(/^@/, '').replace(/^[A-Za-z_]+=/, ''), policy)
333    if (verdict) return { ...verdict, subject: words.join(' ') }
334  }
335
336  return undefined
337}
338
339/** Checks a whole Bash command line; the first rule that fires wins. */
340export function checkCommand(command: string, policy: Policy): Verdict | undefined {
341  for (const { source, pattern } of policy.commands) {
342    if (pattern.test(command)) {
343      return {
344        rule: 'blocked-command',
345        subject: command,
346        reason: `it matches the blocked pattern /${source}/`,
347        hint: 'Find another way, or ask the user to run it.',
348      }
349    }
350  }
351
352  if (policy.destructive) {
353    const whole = checkWhole(command)
354    if (whole) return whole
355  }
356
357  for (const words of splitCommand(command).map(stripPrefixes)) {
358    if (words.length === 0) continue
359    const verdict =
360      (policy.destructive ? checkDestructive(words, policy.rmMode) : undefined) ??
361      (policy.secrets || policy.protect.length > 0 ? checkSecretMentions(words, policy) : undefined)
362    if (verdict) return verdict
363  }
364
365  return undefined
366}
367
368/** The text the model receives in place of the tool's result. */
369export function denyMessage(verdict: Verdict): string {
370  return `guard blocked this (${verdict.rule}): ${verdict.reason}. ${verdict.hint}`
371}
372
373/** The active rules, one per line, for /guard. */
374export function describePolicy(policy: Policy): string[] {
375  const lines: string[] = []
376  if (policy.destructive) {
377    lines.push(
378      `destructive commands: rm -rf (${policy.rmMode === 'any' ? 'any target' : 'dangerous targets'}), git push --force, git reset --hard, git clean -f, chmod -R 777, mkfs/dd to devices, curl|sh`,
379    )
380  }
381  if (policy.secrets) lines.push(`secret files: ${SECRET_GLOBS.join(', ')} (except ${SECRET_EXCEPTIONS.join(', ')})`)
382  if (policy.protect.length > 0) lines.push(`protected paths: ${policy.protect.map(({ source }) => source).join(', ')}`)
383  if (policy.commands.length > 0) lines.push(`blocked patterns: ${policy.commands.map(({ source }) => `/${source}/`).join(', ')}`)
384  if (policy.allow.length > 0) lines.push('allowed paths override the file rules')
385  if (lines.length === 0) lines.push('no rules are on')
386
387  return lines
388}
389
types/index.d.ts 20 lines
1export type GuardBlock = {
2  /** The tool whose call was denied. */
3  tool: string
4  /** The rule that fired. */
5  rule: string
6  /** The command segment or path it matched. */
7  subject: string
8  /** When, in ms since the epoch. */
9  at: number
10}
11
12declare module 'claude-code' {
13  interface PluginState {
14    guard: {
15      isPaused: boolean
16      blocks: GuardBlock[]
17    }
18  }
19}
20