|
| 1 | +--- |
| 2 | +name: programmatic-claude-lockdown |
| 3 | +description: Reference for locking down programmatic Claude invocations (the `claude` CLI in workflows/scripts, the `@anthropic-ai/claude-agent-sdk` `query()` in code). Loads on demand when writing or reviewing any callsite that runs Claude programmatically. Source: https://code.claude.com/docs/en/agent-sdk/permissions. |
| 4 | +user-invocable: false |
| 5 | +allowed-tools: Read, Grep, Glob |
| 6 | +--- |
| 7 | + |
| 8 | +# Programmatic Claude lockdown |
| 9 | + |
| 10 | +**Rule:** every programmatic Claude callsite sets four flags. Skip any one and a future edit silently widens the surface. |
| 11 | + |
| 12 | +## The four flags |
| 13 | + |
| 14 | +| Layer | SDK option | CLI flag | What it does | |
| 15 | +|---|---|---|---| |
| 16 | +| Definition | `tools` | `--tools` | Base set the model is told about. Tools not listed are invisible β no `tool_use` block possible. | |
| 17 | +| Auto-approve | `allowedTools` | `--allowedTools` | Step 4. Listed tools run without invoking `canUseTool`. | |
| 18 | +| Deny | `disallowedTools` | `--disallowedTools` | Step 2. Wins even against `bypassPermissions`. Defense-in-depth. | |
| 19 | +| Mode | `permissionMode: 'dontAsk'` | `--permission-mode dontAsk` | Step 3. Unmatched tools denied without falling through to a missing `canUseTool`. | |
| 20 | + |
| 21 | +The official permission flow (1) hooks β (2) deny rules β (3) permission mode β (4) allow rules β (5) `canUseTool`. In `dontAsk` mode step 5 is skipped β denied. The doc states verbatim: *"`allowedTools` and `disallowedTools` ... control whether a tool call is approved, not whether the tool is available."* Availability is `tools`. |
| 22 | + |
| 23 | +## Recipe β read-only agent (audit, classify, summarize) |
| 24 | + |
| 25 | +```ts |
| 26 | +import { query } from '@anthropic-ai/claude-agent-sdk' |
| 27 | + |
| 28 | +query({ |
| 29 | + prompt: '...', |
| 30 | + options: { |
| 31 | + tools: ['Read', 'Grep', 'Glob'], |
| 32 | + allowedTools: ['Read', 'Grep', 'Glob'], |
| 33 | + disallowedTools: ['Agent', 'Bash', 'Edit', 'NotebookEdit', 'Task', 'WebFetch', 'WebSearch', 'Write'], |
| 34 | + permissionMode: 'dontAsk', |
| 35 | + }, |
| 36 | +}) |
| 37 | +``` |
| 38 | + |
| 39 | +CLI form for workflow YAML / shell scripts: |
| 40 | + |
| 41 | +```yaml |
| 42 | +claude --print \ |
| 43 | + --tools "Read" "Grep" "Glob" \ |
| 44 | + --allowedTools "Read" "Grep" "Glob" \ |
| 45 | + --disallowedTools "Agent" "Bash" "Edit" "NotebookEdit" "Task" "WebFetch" "WebSearch" "Write" \ |
| 46 | + --permission-mode dontAsk \ |
| 47 | + --model "$MODEL" \ |
| 48 | + --max-turns 25 \ |
| 49 | + "<prompt>" |
| 50 | +``` |
| 51 | + |
| 52 | +## Recipe β agent that needs Bash (e.g. `/updating`: pnpm + git + jq) |
| 53 | + |
| 54 | +Narrow `Bash(...)` patterns surgically. Block dangerous Bash patterns explicitly. Fleet rules: no `npx`/`pnpm dlx`/`yarn dlx`; no `curl`/`wget` exfil; no destructive `rm -rf`; no `sudo`. Build the deny list as shell vars so the npx/dlx denials can carry the `# zizmor:` exemption marker (the pre-commit `scanNpxDlx` hook treats those literal strings as the prohibited tools, not as exemptions, unless the line is tagged): |
| 55 | + |
| 56 | +```yaml |
| 57 | +DISALLOW_BASE='Agent Task NotebookEdit WebFetch WebSearch Bash(curl:*) Bash(wget:*) Bash(rm -rf*) Bash(sudo:*)' |
| 58 | +DISALLOW_PKG_EXEC='Bash(npx:*) Bash(pnpm dlx:*) Bash(yarn dlx:*)' # zizmor: documentation-prohibition |
| 59 | +claude --print \ |
| 60 | + --tools "Bash" "Read" "Write" "Edit" "Glob" "Grep" \ |
| 61 | + --allowedTools "Bash(pnpm:*)" "Bash(git:*)" "Bash(jq:*)" "Read" "Write" "Edit" "Glob" "Grep" \ |
| 62 | + --disallowedTools $DISALLOW_BASE $DISALLOW_PKG_EXEC \ |
| 63 | + --permission-mode dontAsk \ |
| 64 | + --model "$MODEL" --max-turns 25 \ |
| 65 | + "<prompt>" |
| 66 | +``` |
| 67 | + |
| 68 | +## Never |
| 69 | + |
| 70 | +- β `permissionMode: 'default'` in headless contexts β falls through to a missing `canUseTool`. Behavior undefined. |
| 71 | +- β `permissionMode: 'bypassPermissions'` / `allowDangerouslySkipPermissions: true`. |
| 72 | +- β Omitting `tools` β SDK default is the full claude_code preset. |
| 73 | +- β `Agent` / `Task` permitted β sub-agents inherit modes and can escape per-subagent restrictions when the parent is `bypassPermissions`/`acceptEdits`/`auto`. |
| 74 | + |
| 75 | +## Reference implementation |
| 76 | + |
| 77 | +`socket-lib/tools/prim/src/disambiguate.mts` β canonical SDK-form callsite. The file header documents each flag against the eval-flow step it enforces. |
| 78 | + |
| 79 | +`socket-lib/tools/prim/test/disambiguate.test.mts` β source-text guards that fail the build if `BASE_TOOLS` widens, if `tools: BASE_TOOLS` is unwired, if `permissionMode` drifts from `'dontAsk'`, or if `bypassPermissions` / `allowDangerouslySkipPermissions: true` ever appears. Mirror this pattern in any new callsite. |
| 80 | + |
| 81 | +## Existing fleet callsites |
| 82 | + |
| 83 | +- `socket-registry/.github/workflows/weekly-update.yml` β two `claude --print` invocations (run `/updating` skill, fix test failures). Bash recipe above. |
| 84 | +- `socket-lib/tools/prim/src/disambiguate.mts` β read-only recipe above (`query()` SDK form). |
0 commit comments