diff --git a/.claude/skills/programmatic-claude-lockdown/SKILL.md b/.claude/skills/programmatic-claude-lockdown/SKILL.md new file mode 100644 index 00000000..f2561013 --- /dev/null +++ b/.claude/skills/programmatic-claude-lockdown/SKILL.md @@ -0,0 +1,84 @@ +--- +name: programmatic-claude-lockdown +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. +user-invocable: false +allowed-tools: Read, Grep, Glob +--- + +# Programmatic Claude lockdown + +**Rule:** every programmatic Claude callsite sets four flags. Skip any one and a future edit silently widens the surface. + +## The four flags + +| Layer | SDK option | CLI flag | What it does | +|---|---|---|---| +| Definition | `tools` | `--tools` | Base set the model is told about. Tools not listed are invisible — no `tool_use` block possible. | +| Auto-approve | `allowedTools` | `--allowedTools` | Step 4. Listed tools run without invoking `canUseTool`. | +| Deny | `disallowedTools` | `--disallowedTools` | Step 2. Wins even against `bypassPermissions`. Defense-in-depth. | +| Mode | `permissionMode: 'dontAsk'` | `--permission-mode dontAsk` | Step 3. Unmatched tools denied without falling through to a missing `canUseTool`. | + +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`. + +## Recipe — read-only agent (audit, classify, summarize) + +```ts +import { query } from '@anthropic-ai/claude-agent-sdk' + +query({ + prompt: '...', + options: { + tools: ['Read', 'Grep', 'Glob'], + allowedTools: ['Read', 'Grep', 'Glob'], + disallowedTools: ['Agent', 'Bash', 'Edit', 'NotebookEdit', 'Task', 'WebFetch', 'WebSearch', 'Write'], + permissionMode: 'dontAsk', + }, +}) +``` + +CLI form for workflow YAML / shell scripts: + +```yaml +claude --print \ + --tools "Read" "Grep" "Glob" \ + --allowedTools "Read" "Grep" "Glob" \ + --disallowedTools "Agent" "Bash" "Edit" "NotebookEdit" "Task" "WebFetch" "WebSearch" "Write" \ + --permission-mode dontAsk \ + --model "$MODEL" \ + --max-turns 25 \ + "" +``` + +## Recipe — agent that needs Bash (e.g. `/updating`: pnpm + git + jq) + +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): + +```yaml +DISALLOW_BASE='Agent Task NotebookEdit WebFetch WebSearch Bash(curl:*) Bash(wget:*) Bash(rm -rf*) Bash(sudo:*)' +DISALLOW_PKG_EXEC='Bash(npx:*) Bash(pnpm dlx:*) Bash(yarn dlx:*)' # zizmor: documentation-prohibition +claude --print \ + --tools "Bash" "Read" "Write" "Edit" "Glob" "Grep" \ + --allowedTools "Bash(pnpm:*)" "Bash(git:*)" "Bash(jq:*)" "Read" "Write" "Edit" "Glob" "Grep" \ + --disallowedTools $DISALLOW_BASE $DISALLOW_PKG_EXEC \ + --permission-mode dontAsk \ + --model "$MODEL" --max-turns 25 \ + "" +``` + +## Never + +- ❌ `permissionMode: 'default'` in headless contexts — falls through to a missing `canUseTool`. Behavior undefined. +- ❌ `permissionMode: 'bypassPermissions'` / `allowDangerouslySkipPermissions: true`. +- ❌ Omitting `tools` — SDK default is the full claude_code preset. +- ❌ `Agent` / `Task` permitted — sub-agents inherit modes and can escape per-subagent restrictions when the parent is `bypassPermissions`/`acceptEdits`/`auto`. + +## Reference implementation + +`socket-lib/tools/prim/src/disambiguate.mts` — canonical SDK-form callsite. The file header documents each flag against the eval-flow step it enforces. + +`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. + +## Existing fleet callsites + +- `socket-registry/.github/workflows/weekly-update.yml` — two `claude --print` invocations (run `/updating` skill, fix test failures). Bash recipe above. +- `socket-lib/tools/prim/src/disambiguate.mts` — read-only recipe above (`query()` SDK form). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b430cd4b..69763f87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,6 @@ concurrency: jobs: ci: name: Run CI Pipeline - uses: SocketDev/socket-registry/.github/workflows/ci.yml@85a2fc0d33af6304246620365de3e7f053035a8d # main + uses: SocketDev/socket-registry/.github/workflows/ci.yml@0fc1abfd5e9ae4396f9fb507371aeea75e2f412c # main with: test-script: 'pnpm run test --all --skip-build' diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index b4534c3e..8a6580ee 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -46,14 +46,14 @@ jobs: echo "Sleeping for $delay seconds..." sleep $delay - - uses: SocketDev/socket-registry/.github/actions/setup-and-install@85a2fc0d33af6304246620365de3e7f053035a8d # main + - uses: SocketDev/socket-registry/.github/actions/setup-and-install@0fc1abfd5e9ae4396f9fb507371aeea75e2f412c # main - name: Configure push credentials env: GH_TOKEN: ${{ github.token }} run: git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" - - uses: SocketDev/socket-registry/.github/actions/setup-git-signing@85a2fc0d33af6304246620365de3e7f053035a8d # main + - uses: SocketDev/socket-registry/.github/actions/setup-git-signing@0fc1abfd5e9ae4396f9fb507371aeea75e2f412c # main with: gpg-private-key: ${{ secrets.BOT_GPG_PRIVATE_KEY }} @@ -145,5 +145,5 @@ jobs: > \`\`\` EOF - - uses: SocketDev/socket-registry/.github/actions/cleanup-git-signing@85a2fc0d33af6304246620365de3e7f053035a8d # main + - uses: SocketDev/socket-registry/.github/actions/cleanup-git-signing@0fc1abfd5e9ae4396f9fb507371aeea75e2f412c # main if: always() diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index ecf5df13..9bd82b9d 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -25,7 +25,7 @@ jobs: permissions: contents: write # To create GitHub releases id-token: write # For npm trusted publishing via OIDC - uses: SocketDev/socket-registry/.github/workflows/provenance.yml@85a2fc0d33af6304246620365de3e7f053035a8d # main + uses: SocketDev/socket-registry/.github/workflows/provenance.yml@0fc1abfd5e9ae4396f9fb507371aeea75e2f412c # main with: debug: ${{ inputs.debug }} dist-tag: ${{ inputs.dist-tag }} diff --git a/.github/workflows/weekly-update.yml b/.github/workflows/weekly-update.yml index 1112eaed..dc58ead3 100644 --- a/.github/workflows/weekly-update.yml +++ b/.github/workflows/weekly-update.yml @@ -10,7 +10,7 @@ permissions: jobs: weekly-update: - uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@85a2fc0d33af6304246620365de3e7f053035a8d # main + uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@0fc1abfd5e9ae4396f9fb507371aeea75e2f412c # main with: test-setup-script: 'pnpm run build' test-script: 'pnpm test' diff --git a/CLAUDE.md b/CLAUDE.md index a59e46aa..793dbf85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ The umbrella rule: never run a git command that mutates state belonging to a pat - **minimumReleaseAge**: NEVER add packages to `minimumReleaseAgeExclude` in CI. Locally, ASK before adding — the age threshold is a security control. - 🚨 **NEVER mention private repos or internal project names** in commits, PR titles/descriptions/comments, issues, release notes, or any public-surface text. Internal codenames, unreleased product names, internal tooling repo names not on the public org page, customer names, partner names — none belong in public surfaces. **Omit the reference entirely.** Don't substitute a placeholder ("an internal tool", "a downstream consumer", etc.) — the placeholder itself is a tell that something is being elided. Rewrite the sentence to not need the reference at all. - 🚨 **NEVER trigger Publish / Release / Provenance / Build-Release workflows** — no `gh workflow run`, `gh workflow dispatch`, or `gh api .../dispatches`. Workflow dispatches are irrevocable: Publish workflows push npm versions (unpublishable after 24h), Build/Release workflows pin GitHub releases by SHA, container workflows push immutable tags. Even build workflows with a `dry_run` input still treat the dispatch itself as the prod trigger. The user runs workflow_dispatch jobs manually after CI passes on the release commit + tag — Claude **never** dispatches them. If the user asks for a publish, tell them to run the command in their own terminal (or the GitHub Actions UI). +- 🚨 **Programmatic Claude calls** (workflows, skills, scripts that invoke `claude` CLI or `@anthropic-ai/claude-agent-sdk`) MUST set all four lockdown flags: `--tools`/`tools`, `--allowedTools`/`allowedTools`, `--disallowedTools`/`disallowedTools`, and `--permission-mode dontAsk`/`permissionMode: 'dontAsk'`. NEVER `default` mode in headless contexts (falls through to a missing `canUseTool` → undefined behavior). NEVER `bypassPermissions`. See `.claude/skills/programmatic-claude-lockdown/SKILL.md` for the recipe + reference impl (`socket-lib/tools/prim/src/disambiguate.mts`). - File existence: ALWAYS `existsSync` from `node:fs`. NEVER `fs.access`, `fs.stat`-for-existence, or an async `fileExists` wrapper. Import form: `import { existsSync, promises as fs } from 'node:fs'`. - Null-prototype objects: ALWAYS use `{ __proto__: null, ...rest }` for config, return, and internal-state objects. Prevents prototype pollution and accidental inheritance. See `src/socket-sdk-class.ts` and `src/file-upload.ts` for examples. - Linear references: NEVER reference Linear issues (e.g. `SOC-123`, `ENG-456`, Linear URLs) in code, code comments, or PR titles/descriptions/review comments. Keep the codebase and PR history tool-agnostic — tracking lives in Linear. diff --git a/external-tools.json b/external-tools.json index 06ef8a73..a3c36c59 100644 --- a/external-tools.json +++ b/external-tools.json @@ -22,7 +22,7 @@ }, "pnpm": { "description": "pnpm — the fleet's package manager.", - "version": "11.0.0-rc.5", + "version": "11.0.0", "packageManager": "pnpm", "repository": "github:pnpm/pnpm", "release": "asset", @@ -34,27 +34,27 @@ "checksums": { "darwin-arm64": { "asset": "pnpm-darwin-arm64.tar.gz", - "sha256": "32a50710ccacfdcf14e6d5995d5368298eec913b0ce3903b9e09b6555f06f4e5" + "sha256": "3620a0fcaf81ecd3aaeccd5965919d90dbc913f4d07a96e11e7cafc2c785054b" }, "darwin-x64": { "asset": "pnpm-darwin-x64.tar.gz", - "sha256": "71dca33f4275da6b43bf1eb40bdc4d876f59a116716eacbf01079c3d985ff85d" + "sha256": "1701748b75187f1333a9c616827943ff84ff46cc42becc156ff6864b9bd0f948" }, "linux-arm64": { "asset": "pnpm-linux-arm64.tar.gz", - "sha256": "2dd04127ff10b1f9dd20bae248b779c77a8ec67e3afa35e7256e5f94abddd493" + "sha256": "1e6d87ebfd7ff169966ff5b3ad71b780b883c68d3e59987df1096dfd8853df75" }, "linux-x64": { "asset": "pnpm-linux-x64.tar.gz", - "sha256": "7ebef4b616ba41fb0d54a207b36508fae3346723283a088b43fc1e038ee6fed0" + "sha256": "9b44acc77ada40fc41b665fde1d57367a5ebec31bd4b1b00598daed195da3e17" }, "win-arm64": { "asset": "pnpm-win32-arm64.zip", - "sha256": "e4a39ad4c251db5e34b18b98561ef25bab5506ad65cad2fa3602af58d1972667" + "sha256": "0746be8e98ca183078d0747559f0cbbd30a13a53eb177f67474eb3c52dc21bc8" }, "win-x64": { "asset": "pnpm-win32-x64.zip", - "sha256": "147485ae2f38c3d1ccf2f5db00d0244416bcd22b9114c02388e6a78f41538fc4" + "sha256": "581e222e622cd0cc4f0ac5f85dd0db76b65117e3b17507979d89e63fdc68edca" } } }, diff --git a/package.json b/package.json index 586274d1..f27ff047 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ }, "engines": { "node": ">=18.20.8", - "pnpm": ">=11.0.0-rc.0" + "pnpm": ">=11.0.0" }, - "packageManager": "pnpm@11.0.0-rc.5" + "packageManager": "pnpm@11.0.0" }