Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/agents/security-reviewer.md
Original file line number Diff line number Diff line change
@@ -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:*)
---

Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/_shared/path-guard-rule.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!--
Shared snippet — the canonical "1 path, 1 reference" rule text.
Synced byte-identical across the Socket fleet via socket-repo-template's
sync-scaffolding.mjs (SHARED_SKILL_FILES).
sync-scaffolding.mts (SHARED_SKILL_FILES).

This file is the source of truth for the rule's wording. Three artifacts
embed (or paraphrase) it:
Expand All @@ -10,7 +10,7 @@ embed (or paraphrase) it:
2. .claude/hooks/path-guard/README.md — what the hook blocks.
3. .claude/skills/path-guard/SKILL.md — what the skill enforces.

If the wording changes here, re-run `node scripts/sync-scaffolding.mjs
If the wording changes here, re-run `node scripts/sync-scaffolding.mts
--all --fix` from socket-repo-template to propagate.
-->

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/path-guard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 84 additions & 0 deletions .claude/skills/programmatic-claude-lockdown/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 \
"<prompt>"
```

## 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 \
"<prompt>"
```

## 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).
57 changes: 57 additions & 0 deletions .claude/skills/promise-race-pitfall/SKILL.md
Original file line number Diff line number Diff line change
@@ -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<Task>()
function startTask(task: Task) {
task.run().then(() => {
const prev = signal
signal = Promise.withResolvers<Task>()
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.
6 changes: 6 additions & 0 deletions .socket-repo-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "./socket-repo-template-schema.json",
"schemaVersion": 1,
"repoName": "socket-sdk-js",
"kind": "single-package"
}
Loading
Loading