Skip to content

Commit 892c2f3

Browse files
authored
docs(claude+skills): CLAUDE.md restructure + new skills (#633)
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.
1 parent 8a9d08e commit 892c2f3

12 files changed

Lines changed: 821 additions & 174 deletions

File tree

.claude/agents/security-reviewer.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: security-reviewer
3-
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.
3+
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.
44
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:*)
55
---
66

@@ -18,7 +18,7 @@ Apply these rules from CLAUDE.md exactly:
1818

1919
1. **Secrets**: Hardcoded API keys, passwords, tokens, private keys in code or config
2020
2. **Injection**: Command injection via shell: true or string interpolation in spawn/exec. Path traversal in file operations.
21-
3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing minimumReleaseAge bypass justification. # zizmor: documentation-checklist
21+
3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing soak-window bypass justification (pnpm-workspace.yaml `minimumReleaseAgeExclude`). # zizmor: documentation-checklist
2222
4. **File operations**: fs.rm without safeDelete. process.chdir usage. fetch() usage (must use lib's httpRequest).
2323
5. **GitHub Actions**: Unpinned action versions (must use full SHA). Secrets outside env blocks. Template injection from untrusted inputs.
2424
6. **Error handling**: Sensitive data in error messages. Stack traces exposed to users.

.claude/skills/_shared/path-guard-rule.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!--
22
Shared snippet — the canonical "1 path, 1 reference" rule text.
33
Synced byte-identical across the Socket fleet via socket-repo-template's
4-
sync-scaffolding.mjs (SHARED_SKILL_FILES).
4+
sync-scaffolding.mts (SHARED_SKILL_FILES).
55
66
This file is the source of truth for the rule's wording. Three artifacts
77
embed (or paraphrase) it:
@@ -10,7 +10,7 @@ embed (or paraphrase) it:
1010
2. .claude/hooks/path-guard/README.md — what the hook blocks.
1111
3. .claude/skills/path-guard/SKILL.md — what the skill enforces.
1212
13-
If the wording changes here, re-run `node scripts/sync-scaffolding.mjs
13+
If the wording changes here, re-run `node scripts/sync-scaffolding.mts
1414
--all --fix` from socket-repo-template to propagate.
1515
-->
1616

.claude/skills/path-guard/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: path-guard
33
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.
44
user-invocable: true
5-
allowed-tools: Task, Bash, Read, Edit, Write, Grep, Glob, AskUserQuestion
5+
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:*)
66
---
77

88
# path-guard
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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).
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
name: promise-race-pitfall
3+
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.
4+
---
5+
6+
# Promise.race in loops — the handler-leak pitfall
7+
8+
**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).
9+
10+
## Patterns
11+
12+
- **Safe** — both arms created per call:
13+
14+
```ts
15+
const value = await Promise.race([
16+
fetchSomething(),
17+
new Promise((_, r) => setTimeout(() => r(new Error('timeout')), 5000)),
18+
])
19+
```
20+
21+
- **Leaky**`pool` survives across iterations, accumulating handlers:
22+
23+
```ts
24+
while (queue.length) {
25+
const winner = await Promise.race(pool) // ← N handlers per arm by iteration N
26+
pool = pool.filter(p => p !== winner)
27+
}
28+
```
29+
30+
Same hazard for `Promise.any` and any long-lived arm such as an interrupt signal.
31+
32+
## The fix
33+
34+
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.
35+
36+
```ts
37+
let signal = Promise.withResolvers<Task>()
38+
function startTask(task: Task) {
39+
task.run().then(() => {
40+
const prev = signal
41+
signal = Promise.withResolvers<Task>()
42+
prev.resolve(task)
43+
})
44+
}
45+
while (queue.length) {
46+
// launch up to N tasks
47+
while (running < N && queue.length) startTask(queue.shift()!)
48+
const finished = await signal.promise
49+
running -= 1
50+
}
51+
```
52+
53+
The arm being awaited is *always fresh*; nothing accumulates handlers.
54+
55+
## Quick check
56+
57+
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.

.socket-repo-template.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "./socket-repo-template-schema.json",
3+
"schemaVersion": 1,
4+
"repoName": "socket-sdk-js",
5+
"kind": "single-package"
6+
}

0 commit comments

Comments
 (0)