From f4ddcfe87d44eb13d5f469a48c9366929073c1d3 Mon Sep 17 00:00:00 2001 From: jdalton Date: Tue, 5 May 2026 15:14:47 -0700 Subject: [PATCH] docs(claude+skills): CLAUDE.md restructure + new skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synced from socket-repo-template canonical. CLAUDE.md moves to the fleet-canonical / project-specific layout (public-surface hygiene, parallel-session safeguards, code style, tooling) with sdk-specific extensions below. Skills added: - programmatic-claude-lockdown — reference for the four-flag lockdown pattern when invoking Claude headlessly (CLI in workflows, agent-sdk query() in code) - promise-race-pitfall — reference for the Promise.race cross-iteration handler-leak bug Skills updated: - path-guard — SKILL.md drift; _shared/path-guard-rule.md update Doctrine references: - docs/references/inclusive-language.md - docs/references/sorting.md Repo-template integration: - .socket-repo-template.json — repo-particular kind config - scripts/socket-repo-template-{schema,emit-schema}.mts — schema tooling - socket-repo-template-schema.json — emitted JSON schema Splits content out of #630. --- .claude/agents/security-reviewer.md | 4 +- .claude/skills/_shared/path-guard-rule.md | 4 +- .claude/skills/path-guard/SKILL.md | 2 +- .../programmatic-claude-lockdown/SKILL.md | 84 ++++++ .claude/skills/promise-race-pitfall/SKILL.md | 57 ++++ .socket-repo-template.json | 6 + CLAUDE.md | 252 ++++++----------- docs/references/inclusive-language.md | 34 +++ docs/references/sorting.md | 16 ++ scripts/socket-repo-template-emit-schema.mts | 44 +++ scripts/socket-repo-template-schema.mts | 263 ++++++++++++++++++ socket-repo-template-schema.json | 229 +++++++++++++++ 12 files changed, 821 insertions(+), 174 deletions(-) create mode 100644 .claude/skills/programmatic-claude-lockdown/SKILL.md create mode 100644 .claude/skills/promise-race-pitfall/SKILL.md create mode 100644 .socket-repo-template.json create mode 100644 docs/references/inclusive-language.md create mode 100644 docs/references/sorting.md create mode 100644 scripts/socket-repo-template-emit-schema.mts create mode 100644 scripts/socket-repo-template-schema.mts create mode 100644 socket-repo-template-schema.json diff --git a/.claude/agents/security-reviewer.md b/.claude/agents/security-reviewer.md index 1d35eabd..04d4b138 100644 --- a/.claude/agents/security-reviewer.md +++ b/.claude/agents/security-reviewer.md @@ -1,6 +1,6 @@ --- name: security-reviewer -description: Reviews findings from AgentShield + zizmor against socket-sdk-js's CLAUDE.md security rules and grades the result A-F. Spawned by the security-scan skill after the static scans run. +description: Reviews findings from AgentShield + zizmor against the project's CLAUDE.md security rules and grades the result A-F. Spawned by the security-scan skill after the static scans run. tools: Read, Grep, Glob, Bash(git:*), Bash(rg:*), Bash(grep:*), Bash(find:*), Bash(ls:*), Bash(pnpm exec agentshield:*), Bash(zizmor:*), Bash(command -v:*), Bash(cat:*), Bash(head:*), Bash(tail:*) --- @@ -18,7 +18,7 @@ Apply these rules from CLAUDE.md exactly: 1. **Secrets**: Hardcoded API keys, passwords, tokens, private keys in code or config 2. **Injection**: Command injection via shell: true or string interpolation in spawn/exec. Path traversal in file operations. -3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing minimumReleaseAge bypass justification. # zizmor: documentation-checklist +3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing soak-window bypass justification (pnpm-workspace.yaml `minimumReleaseAgeExclude`). # zizmor: documentation-checklist 4. **File operations**: fs.rm without safeDelete. process.chdir usage. fetch() usage (must use lib's httpRequest). 5. **GitHub Actions**: Unpinned action versions (must use full SHA). Secrets outside env blocks. Template injection from untrusted inputs. 6. **Error handling**: Sensitive data in error messages. Stack traces exposed to users. diff --git a/.claude/skills/_shared/path-guard-rule.md b/.claude/skills/_shared/path-guard-rule.md index fa42a32e..2447f8b7 100644 --- a/.claude/skills/_shared/path-guard-rule.md +++ b/.claude/skills/_shared/path-guard-rule.md @@ -1,7 +1,7 @@ diff --git a/.claude/skills/path-guard/SKILL.md b/.claude/skills/path-guard/SKILL.md index 747ad02b..8ff21c2b 100644 --- a/.claude/skills/path-guard/SKILL.md +++ b/.claude/skills/path-guard/SKILL.md @@ -2,7 +2,7 @@ name: path-guard description: Audit and fix path duplication in this Socket repo. Apply the strict "1 path, 1 reference" rule — every build/test/runtime/config path is constructed exactly once; everywhere else references the constructed value. Default mode finds and fixes; `check` mode reports only; `install` mode drops the gate + hook + rule into a fresh repo. user-invocable: true -allowed-tools: Task, Bash, Read, Edit, Write, Grep, Glob, AskUserQuestion +allowed-tools: Task, Read, Edit, Write, Grep, Glob, AskUserQuestion, Bash(pnpm run check:*), Bash(node scripts/check-paths:*), Bash(rg:*), Bash(grep:*), Bash(find:*), Bash(git:*) --- # path-guard 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/.claude/skills/promise-race-pitfall/SKILL.md b/.claude/skills/promise-race-pitfall/SKILL.md new file mode 100644 index 00000000..d38f3c2a --- /dev/null +++ b/.claude/skills/promise-race-pitfall/SKILL.md @@ -0,0 +1,57 @@ +--- +name: promise-race-pitfall +description: Reference for the `Promise.race` cross-iteration handler-leak bug. Loads on demand when writing or reviewing concurrency code that uses `Promise.race`, `Promise.any`, or hand-rolled concurrency limiters. +--- + +# Promise.race in loops — the handler-leak pitfall + +**Never re-race the same pool of promises across loop iterations.** Each call to `Promise.race([A, B, …])` attaches fresh `.then` handlers to every arm. A promise that survives N iterations accumulates N handler sets. See [nodejs/node#17469](https://github.com/nodejs/node/issues/17469) and [`@watchable/unpromise`](https://github.com/watchable/unpromise). + +## Patterns + +- **Safe** — both arms created per call: + + ```ts + const value = await Promise.race([ + fetchSomething(), + new Promise((_, r) => setTimeout(() => r(new Error('timeout')), 5000)), + ]) + ``` + +- **Leaky** — `pool` survives across iterations, accumulating handlers: + + ```ts + while (queue.length) { + const winner = await Promise.race(pool) // ← N handlers per arm by iteration N + pool = pool.filter(p => p !== winner) + } + ``` + + Same hazard for `Promise.any` and any long-lived arm such as an interrupt signal. + +## The fix + +Use a single-waiter "slot available" signal. Each task's `.then` resolves a one-shot `promiseWithResolvers` that the loop awaits, then replaces. No persistent pool, nothing to stack. + +```ts +let signal = Promise.withResolvers() +function startTask(task: Task) { + task.run().then(() => { + const prev = signal + signal = Promise.withResolvers() + prev.resolve(task) + }) +} +while (queue.length) { + // launch up to N tasks + while (running < N && queue.length) startTask(queue.shift()!) + const finished = await signal.promise + running -= 1 +} +``` + +The arm being awaited is *always fresh*; nothing accumulates handlers. + +## Quick check + +Before merging concurrency code, ask: *does any arm of a `Promise.race`/`Promise.any` outlive the call?* If yes, refactor to the single-waiter signal. diff --git a/.socket-repo-template.json b/.socket-repo-template.json new file mode 100644 index 00000000..2668dabe --- /dev/null +++ b/.socket-repo-template.json @@ -0,0 +1,6 @@ +{ + "$schema": "./socket-repo-template-schema.json", + "schemaVersion": 1, + "repoName": "socket-sdk-js", + "kind": "single-package" +} diff --git a/CLAUDE.md b/CLAUDE.md index a59e46aa..6b609fd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,150 +2,132 @@ 🚨 **MANDATORY**: Act as principal-level engineer with deep expertise in TypeScript, Node.js, and SDK development. -## USER CONTEXT + -- Identify users by git credentials; use their actual name, never "the user" -- Use "you/your" when speaking directly; use names when referencing contributions +## 📚 Fleet Standards -## 🚨 PARALLEL CLAUDE SESSIONS - WORKTREE REQUIRED +### Identifying users -**This repo may have multiple Claude sessions running concurrently against the same checkout, against parallel git worktrees, or against sibling clones.** Several common git operations are hostile to that and silently destroy or hijack the other session's work. +Identify users by git credentials and use their actual name. Use "you/your" when speaking directly; use names when referencing contributions. -- **FORBIDDEN in the primary checkout** (the one another Claude may be editing): - - `git stash` — shared stash store; another session can `pop` yours. - - `git add -A` / `git add .` — sweeps files belonging to other sessions. - - `git checkout ` / `git switch ` — yanks the working tree out from under another session. - - `git reset --hard` against a non-HEAD ref — discards another session's commits. -- **REQUIRED for branch work**: spawn a worktree instead of switching branches in place. Each worktree has its own HEAD, so branch operations inside it are safe. +### Parallel Claude sessions - ```bash - # From the primary checkout — does NOT touch the working tree here. - git worktree add -b ../- main - cd ../- - # edit, commit, push from here; the primary checkout is untouched. - cd - - git worktree remove ../- - ``` +This repo may have multiple Claude sessions running concurrently against the same checkout, against parallel git worktrees, or against sibling clones. Several common git operations are hostile to that. -- **REQUIRED for staging**: surgical `git add […]` with explicit paths. Never `-A` / `.`. -- **If you need a quick WIP save**: commit on a new branch from inside a worktree, not a stash. -- **NEVER revert files you didn't touch.** If `git status` shows files you didn't modify, those belong to another session, an upstream pull, or a hook side-effect — leave them alone. Specifically: do not run `git checkout -- ` to "clean up" the diff before committing, and do not include unrelated paths in `git add`. Stage only the explicit files you edited. +**Forbidden in the primary checkout:** -The umbrella rule: never run a git command that mutates state belonging to a path other than the file you just edited. +- `git stash` — shared store; another session can `pop` yours +- `git add -A` / `git add .` — sweeps files from other sessions +- `git checkout ` / `git switch ` — yanks the working tree out from under another session +- `git reset --hard` against a non-HEAD ref — discards another session's commits -## 📚 SHARED STANDARDS +**Required for branch work:** spawn a worktree. -- Commits: [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) `(): ` — NO AI attribution -- **Open PRs:** when adding commits to an OPEN PR, ALWAYS update the PR title and description to match the new scope. A title like `chore: foo` after you've added security-fix and docs commits to it is now a lie. Use `gh pr edit --title "..." --body "..."` (or `--body-file`) and rewrite the body so it reflects every commit on the branch, grouped by theme. The reviewer should be able to read the PR description and know what's in it without scrolling commits. -- Scripts: Prefer `pnpm run foo --flag` over `foo:bar` scripts -- Dependencies: After `package.json` edits, run `pnpm install` -- Backward Compatibility: 🚨 FORBIDDEN to maintain — actively remove when encountered -- 🚨 **NEVER use `npx`, `pnpm dlx`, or `yarn dlx`** — use `pnpm exec ` or `pnpm run