Skip to content

Commit d3d69a7

Browse files
authored
docs(claude+skills): CLAUDE.md restructure + path-guard + new skills (#1301)
* docs(claude+skills): CLAUDE.md restructure + path-guard + new skills Synced from socket-repo-template canonical. The fleet CLAUDE.md moved to a fleet-canonical / project-specific layout — public-surface hygiene rules, parallel-session safeguards, code style, and tooling go in CLAUDE.md; project-specific extensions (build commands, test targets, repo-particular conventions) go below. Skills added: - path-guard — audit and fix path duplication ("1 path, 1 reference") - programmatic-claude-lockdown — reference for locking down headless Claude invocations (claude CLI in workflows, agent-sdk query() in code) per the four-flag lockdown pattern - promise-race-pitfall — reference for the Promise.race cross-iteration handler-leak bug Skills updated: - security-scan — wires AgentShield + zizmor + Socket CLI dependency scanning, A-F graded report Path-guard infra: - scripts/check-paths.mts — repo-level path-duplication scanner - .github/paths-allowlist.yml — known-acceptable duplicates - .claude/skills/path-guard/reference/* — templates for new repos Doctrine references: - docs/references/inclusive-language.md — substitution table - docs/references/sorting.md — alphanumeric sort rules 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 #1286. Companion split PR #1300 covers hooks + harness config; deps_misc bucket folds into #1285 SHA cascade per project direction. * fix(check-paths): Rule F requires >=2 distinct files, not just >=2 hits Cursor Bugbot caught: `checkRuleF` grouped Rule-A findings purely by string-literal shape and promoted to Rule F whenever count >= 2, without checking distinct files. Two hand-builds of the same path shape in the SAME file would be incorrectly flagged as 'cross-file repetition' — but Rule F's whole point is cross-file duplication. Fix: build a Set of distinct file paths and gate the promotion on size >= 2. Also include the file count in the message (`in N files (M places)`) so the reviewer knows both numbers. Synced to socket-repo-template canonical.
1 parent 9cb69c4 commit d3d69a7

18 files changed

Lines changed: 3122 additions & 161 deletions

.claude/agents/security-reviewer.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
---
2+
name: security-reviewer
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.
4+
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:*)
5+
---
6+
17
You are a security reviewer for Socket Security Node.js repositories.
28

39
Apply these rules from CLAUDE.md exactly:
410

511
**Safe File Operations**: Use safeDelete()/safeDeleteSync() from @socketsecurity/lib/fs. NEVER fs.rm(), fs.rmSync(), or rm -rf. Use os.tmpdir() + fs.mkdtemp() for temp dirs. NEVER use fetch() — use httpJson/httpText/httpRequest from @socketsecurity/lib/http-request.
612

7-
**Absolute Rules**: NEVER use npx, pnpm dlx, or yarn dlx. Use pnpm exec or pnpm run with pinned devDeps.
13+
**Absolute Rules**: NEVER use npx, pnpm dlx, or yarn dlx. Use pnpm exec or pnpm run with pinned devDeps. # zizmor: documentation-prohibition
814

915
**Work Safeguards**: Scripts modifying multiple files must have backup/rollback. Git operations that rewrite history require explicit confirmation.
1016

1117
**Review checklist:**
1218

1319
1. **Secrets**: Hardcoded API keys, passwords, tokens, private keys in code or config
1420
2. **Injection**: Command injection via shell: true or string interpolation in spawn/exec. Path traversal in file operations.
15-
3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing minimumReleaseAge bypass justification.
21+
3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing soak-window bypass justification (pnpm-workspace.yaml `minimumReleaseAgeExclude`). # zizmor: documentation-checklist
1622
4. **File operations**: fs.rm without safeDelete. process.chdir usage. fetch() usage (must use lib's httpRequest).
1723
5. **GitHub Actions**: Unpinned action versions (must use full SHA). Secrets outside env blocks. Template injection from untrusted inputs.
1824
6. **Error handling**: Sensitive data in error messages. Stack traces exposed to users.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!--
2+
Shared snippet — the canonical "1 path, 1 reference" rule text.
3+
Synced byte-identical across the Socket fleet via socket-repo-template's
4+
sync-scaffolding.mts (SHARED_SKILL_FILES).
5+
6+
This file is the source of truth for the rule's wording. Three artifacts
7+
embed (or paraphrase) it:
8+
9+
1. CLAUDE.md — every Socket repo's instructions to Claude.
10+
2. .claude/hooks/path-guard/README.md — what the hook blocks.
11+
3. .claude/skills/path-guard/SKILL.md — what the skill enforces.
12+
13+
If the wording changes here, re-run `node scripts/sync-scaffolding.mts
14+
--all --fix` from socket-repo-template to propagate.
15+
-->
16+
17+
## 1 path, 1 reference
18+
19+
**A path is *constructed* exactly once. Everywhere else *references* the constructed value.**
20+
21+
Referencing a single computed path many times is fine — that's the whole point of computing it once. What's banned is *re-constructing* the same path in multiple places, because that's where drift is born. Three concrete shapes:
22+
23+
1. **Within a package** — every script, test, and lib file that needs a build path imports it from the package's `scripts/paths.mts` (or `lib/paths.mts`). No `path.join('build', mode, ...)` outside that module.
24+
25+
2. **Across packages** — when package B consumes package A's output, B imports A's `paths.mts` via the workspace `exports` field. Never `path.join(PKG, '..', '<sibling>', 'build', ...)`. The R28 yoga/ink bug — ink hand-building yoga's wasm path and missing the `wasm/` segment — is the canonical failure mode this rule prevents.
26+
27+
3. **Workflows, Dockerfiles, shell scripts** — they can't `import` TS, so they construct the string once and reference it everywhere downstream. Workflows: a "Compute paths" step exposes `steps.paths.outputs.final_dir`; later steps read `${{ steps.paths.outputs.final_dir }}`. Dockerfiles/shell: assign once to a variable, reference by name thereafter. Each canonical construction carries a comment naming the source-of-truth `paths.mts` so the YAML can't drift from TS without a flagged change. **Re-building** the same path in a second step is the violation, not referring to the constructed value many times.
28+
29+
Comments that re-state a full path are forbidden. The import statement IS the comment. Docs and READMEs may describe the structure ("output goes under the Final dir") but should not encode a complete `build/<mode>/<platform-arch>/out/Final/binary` string — encoded paths get parsed by tools and silently rot.
30+
31+
Code execution takes priority over docs: violations in `.mts`/`.cts`, Makefiles, Dockerfiles, workflow YAML, and shell scripts are blocking. README and doc-comment violations are advisory unless they contain a fully-qualified path with no parametric placeholders.
32+
33+
### Three-level enforcement
34+
35+
- **Hook**`.claude/hooks/path-guard/` blocks `Edit`/`Write` calls that would introduce a violation in a `.mts`/`.cts` file. Refusal at edit time stops new duplication from landing.
36+
- **Gate**`scripts/check-paths.mts` runs in `pnpm check`. Fails the build on any violation that isn't allowlisted.
37+
- **Skill**`/path-guard` audits the repo and fixes findings; `/path-guard check` reports only; `/path-guard install` drops the gate + hook + rule into a fresh repo.
38+
39+
The mantra is intentionally short so it sticks: **1 path, 1 reference**. When in doubt, find the canonical owner and import from it.

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

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
name: path-guard
3+
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.
4+
user-invocable: true
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:*)
6+
---
7+
8+
# path-guard
9+
10+
**Mantra: 1 path, 1 reference.** A path is constructed exactly once; everywhere else references the constructed value. Re-constructing the same path twice is the violation, not referencing the constructed value many times.
11+
12+
## Modes
13+
14+
- `/path-guard` — full audit-and-fix conversion of the current repo (default).
15+
- `/path-guard check` — read-only audit, report violations, no fixes.
16+
- `/path-guard fix <id>` — fix a single finding from a prior `check` run, by index.
17+
- `/path-guard install` — drop the gate + hook + rule + allowlist into a fresh repo (for new Socket repos).
18+
19+
## Three-level enforcement
20+
21+
The strategy lives in three artifacts that ship together:
22+
23+
1. **CLAUDE.md rule** — the mantra and detection rules in plain language. Every Socket repo's CLAUDE.md carries `## 1 path, 1 reference`. Synced from `.claude/skills/_shared/path-guard-rule.md`.
24+
2. **Hook**`.claude/hooks/path-guard/index.mts` runs `PreToolUse` on `Edit`/`Write` of `.mts`/`.cts` files. Blocks new violations at edit time. Mandatory across the fleet.
25+
3. **Gate**`scripts/check-paths.mts` runs in `pnpm check` (and CI). Whole-repo scan. Fails the build on any unsanctioned violation.
26+
27+
The hook and gate share their stage / build-root / mode / sibling-package vocabulary via `.claude/hooks/path-guard/segments.mts` — a single canonical source. Adding a new stage segment or fleet package means editing one file; the two consumers can never drift on what counts as a build-output path.
28+
29+
This skill is the *audit-and-fix workflow* that makes a repo conform initially and validates conformance over time.
30+
31+
## Detection rules
32+
33+
The gate enforces six rules. The hook enforces a subset (A and B) since it sees only one diff at a time.
34+
35+
| Rule | What it catches | Where checked |
36+
|---|---|---|
37+
| **A** | Multi-stage `path.join(...)` constructed inline. Two or more "stage" segments (Final, Release, Stripped, Compressed, Optimized, Synced, wasm, downloaded), or one stage + build-root + mode. | `.mts`/`.cts` files outside a `paths.mts`. Hook + gate. |
38+
| **B** | Cross-package traversal: `path.join(*, '..', '<sibling-package>', 'build', ...)` reaching into a sibling's output instead of importing via `exports`. | `.mts`/`.cts` files. Hook + gate. |
39+
| **C** | Workflow YAML constructs the same path string in 2+ steps outside a "Compute paths" step. | `.github/workflows/*.yml`. Gate. |
40+
| **D** | Comment encodes a fully-qualified multi-stage path string (e.g. `# build/dev/darwin-arm64/out/Final/binary`). | `.github/workflows/*.yml`. Gate. |
41+
| **F** | Same path shape constructed in 2+ different files. | All scanned files. Gate. |
42+
| **G** | Hand-built multi-stage path constructed 2+ times in the same Makefile/Dockerfile/shell stage. | `Makefile`, `*.mk`, `*.Dockerfile`, `Dockerfile.*`, `*.sh`. Gate. |
43+
44+
Comments may describe path *structure* with placeholders (`<mode>/<arch>` or `${BUILD_MODE}/${PLATFORM_ARCH}`) but should not encode a complete literal path string. Code execution takes priority over docs: violations in `.mts`, Makefiles, Dockerfiles, workflow YAML, shell scripts are blocking.
45+
46+
## Mode: audit-and-fix (default)
47+
48+
When invoked as `/path-guard` with no arg:
49+
50+
1. **Setup** — spawn a worktree off `main` per `CLAUDE.md` parallel-sessions rule:
51+
```bash
52+
git worktree add -b paths-audit ../<repo>-paths-audit main
53+
cd ../<repo>-paths-audit
54+
```
55+
56+
2. **Audit** — run the gate to enumerate findings:
57+
```bash
58+
pnpm run check:paths --json > /tmp/paths-findings.json
59+
pnpm run check:paths --explain # human-readable
60+
```
61+
62+
3. **Fix loop** — for each finding, apply the matching pattern below. After each fix, re-run the gate. Stop iterating when `pnpm run check:paths` exits 0.
63+
64+
4. **Verify** — run the full check suite + zizmor on any modified workflow:
65+
```bash
66+
pnpm check
67+
for w in .github/workflows/*.yml; do zizmor "$w"; done
68+
```
69+
70+
5. **Commit and push** — group fixes by logical category (workflows, code, Dockerfiles). Push directly to `main` for repos that allow direct push, or open a PR for repos that require it (socket-cli, socket-sdk-js, socket-registry per their CLAUDE.md / memory entries).
71+
72+
## Fix patterns
73+
74+
### Rule A — Multi-stage path constructed inline (in `.mts`/`.cts`)
75+
76+
**Bad**:
77+
```ts
78+
const finalBinary = path.join(PACKAGE_ROOT, 'build', BUILD_MODE, PLATFORM_ARCH, 'out', 'Final', 'binary')
79+
```
80+
81+
**Fix**: move the construction into the package's `scripts/paths.mts` (or `lib/paths.mts`), or use a build-infra helper:
82+
```ts
83+
// In packages/foo/scripts/paths.mts:
84+
export function getBuildPaths(mode, platformArch) {
85+
// ... constructs once ...
86+
return { outputFinalBinary: path.join(PACKAGE_ROOT, 'build', mode, platformArch, 'out', 'Final', binaryName) }
87+
}
88+
89+
// In the consumer:
90+
import { getBuildPaths } from './paths.mts'
91+
const { outputFinalBinary } = getBuildPaths(mode, platformArch)
92+
```
93+
94+
For binsuite tools (binpress/binflate/binject) the canonical helper is `getFinalBinaryPath(packageRoot, mode, platformArch, binaryName)` from `build-infra/lib/paths`. For download caches use `getDownloadedDir(packageRoot)`.
95+
96+
### Rule B — Cross-package traversal
97+
98+
**Bad**:
99+
```ts
100+
const liefDir = path.join(PACKAGE_ROOT, '..', 'lief-builder', 'build', mode, platformArch, 'out', 'Final', 'lief')
101+
```
102+
103+
**Fix**: declare the workspace dep, expose `paths.mts` via the producer's `exports`, import the helper:
104+
105+
1. In producer's `package.json`:
106+
```json
107+
"exports": {
108+
"./scripts/paths": "./scripts/paths.mts"
109+
}
110+
```
111+
2. In consumer's `package.json` `dependencies`:
112+
```json
113+
"lief-builder": "workspace:*"
114+
```
115+
3. In consumer:
116+
```ts
117+
import { getBuildPaths as getLiefBuildPaths } from 'lief-builder/scripts/paths'
118+
const { outputFinalDir } = getLiefBuildPaths(mode, platformArch)
119+
```
120+
121+
### Rule C — Workflow path repetition
122+
123+
**Bad** (3 steps each rebuilding the same path):
124+
```yaml
125+
- name: Step A
126+
run: cd packages/foo/build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final && do-thing-1
127+
- name: Step B
128+
run: cd packages/foo/build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final && do-thing-2
129+
- name: Step C
130+
run: cd packages/foo/build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final && do-thing-3
131+
```
132+
133+
**Fix**: add a "Compute <pkg> paths" step early in the job that constructs the path once, expose via `$GITHUB_OUTPUT`, reference downstream:
134+
135+
```yaml
136+
- name: Compute foo paths
137+
id: paths
138+
env:
139+
BUILD_MODE: ${{ steps.build-mode.outputs.mode }}
140+
PLATFORM_ARCH: ${{ steps.platform-arch.outputs.platform_arch }}
141+
run: |
142+
PACKAGE_DIR="packages/foo"
143+
PLATFORM_BUILD_DIR="${PACKAGE_DIR}/build/${BUILD_MODE}/${PLATFORM_ARCH}"
144+
FINAL_DIR="${PLATFORM_BUILD_DIR}/out/Final"
145+
{
146+
echo "package_dir=${PACKAGE_DIR}"
147+
echo "platform_build_dir=${PLATFORM_BUILD_DIR}"
148+
echo "final_dir=${FINAL_DIR}"
149+
} >> "$GITHUB_OUTPUT"
150+
151+
- name: Step A
152+
env:
153+
FINAL_DIR: ${{ steps.paths.outputs.final_dir }}
154+
run: cd "$FINAL_DIR" && do-thing-1
155+
# ... etc
156+
```
157+
158+
For paths used inside `working-directory: packages/foo` steps, expose a `_rel` companion (e.g. `final_dir_rel=build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final`) and reference that.
159+
160+
### Rule D — Comment-encoded paths
161+
162+
**Bad**:
163+
```yaml
164+
# Path: packages/foo/build/dev/darwin-arm64/out/Final/binary
165+
COPY --from=builder /build/.../out/Final/binary /out/Final/binary
166+
```
167+
168+
**Fix**: cite the canonical `paths.mts` instead of duplicating the string:
169+
```yaml
170+
# Layout owned by packages/foo/scripts/paths.mts:getBuildPaths().
171+
COPY --from=builder /build/packages/foo/build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final/binary /out/Final/binary
172+
```
173+
174+
The comment may describe structure (`<mode>/<arch>`) but should not be a parsable literal path.
175+
176+
### Rule G — Dockerfile/Makefile/shell duplicate construction
177+
178+
**Bad** (Dockerfile reconstructs the path 3 times in the same stage):
179+
```dockerfile
180+
RUN mkdir -p build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final && \
181+
cp src build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final/output && \
182+
ls build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final/
183+
```
184+
185+
**Fix**: declare an `ENV` once, reference everywhere:
186+
```dockerfile
187+
# Layout owned by packages/foo/scripts/paths.mts.
188+
ENV FINAL_DIR=build/${BUILD_MODE}/${PLATFORM_ARCH}/out/Final
189+
RUN mkdir -p "$FINAL_DIR" && cp src "$FINAL_DIR/output" && ls "$FINAL_DIR/"
190+
```
191+
192+
Each Dockerfile `FROM` stage is its own scope — ENV from the build stage doesn't reach a subsequent `FROM scratch AS export` stage. The gate accounts for this.
193+
194+
## Mode: check (read-only)
195+
196+
When invoked as `/path-guard check`:
197+
198+
```bash
199+
pnpm run check:paths --explain
200+
```
201+
202+
Print the gate's findings without making any edits. Exit 0 if clean, 1 if findings present. Useful for CI / pre-merge inspection.
203+
204+
## Allowlisting a finding
205+
206+
When a genuine exemption is needed (rare — most "false positives" should be reported as gate bugs), add an entry to `.github/paths-allowlist.yml`. Two ways to pin the entry to a specific site:
207+
208+
- **`line:`** — exact line number. Strict; a single-line edit above shifts the entry off-target and the finding re-surfaces.
209+
- **`snippet_hash:`** — 12-char SHA-256 prefix of the offending snippet (whitespace-normalized). Drift-resistant: survives reformatting, but any content-changing edit invalidates it. Get the hash:
210+
```bash
211+
pnpm run check:paths --show-hashes
212+
```
213+
214+
Both may be set — either matching is sufficient. Prefer `snippet_hash` over raw `line:` when the exemption is expected to outlive routine reformatting; prefer `line:` when you specifically *want* the entry to fall off after any nearby edit.
215+
216+
## Mode: install (new repo)
217+
218+
When invoked as `/path-guard install` on a Socket repo that doesn't yet have the gate:
219+
220+
1. Copy the gate file from this skill's reference dir:
221+
```bash
222+
cp .claude/skills/path-guard/reference/check-paths.mts.tmpl scripts/check-paths.mts
223+
```
224+
2. Copy the empty allowlist:
225+
```bash
226+
cp .claude/skills/path-guard/reference/paths-allowlist.yml.tmpl .github/paths-allowlist.yml
227+
```
228+
3. Add `"check:paths": "node scripts/check-paths.mts"` to `package.json`.
229+
4. Wire `runPathHygieneCheck()` into `scripts/check.mts` (after the existing checks).
230+
5. Append the rule snippet from `.claude/skills/_shared/path-guard-rule.md` to the repo's `CLAUDE.md` if a `1 path, 1 reference` section is missing.
231+
6. Add the hook entry to `.claude/settings.json` `PreToolUse` matcher `Edit|Write`:
232+
```json
233+
{ "type": "command", "command": "node .claude/hooks/path-guard/index.mts" }
234+
```
235+
7. Run the gate against the repo. Triage findings as you would in audit-and-fix mode.
236+
237+
## Tie-in with quality-scan
238+
239+
The `/quality-scan` skill should call `pnpm run check:paths --json` as one of its sub-scans and surface findings as part of its A-F graded report. Failures roll into the overall quality grade. The full audit-and-fix workflow lives here; quality-scan just *detects* during periodic scans.
240+
241+
## Reference patterns
242+
243+
When converting a repo to the strategy, the patterns I keep reusing:
244+
245+
- **TS-first packages**: each package owns a `scripts/paths.mts` with `PACKAGE_ROOT`, `BUILD_ROOT`, `getBuildPaths(mode, platformArch)` returning at minimum `outputFinalDir` and `outputFinalBinary`/`outputFinalFile`.
246+
- **Cross-package consumers**: `package.json` `exports` whitelists `./scripts/paths`. Consumer adds `"<producer>: workspace:*"` and imports.
247+
- **Workflows**: each job has a "Compute <pkg> paths" step (`id: paths`) early in the job. Step outputs include `package_dir`, `platform_build_dir`, `final_dir`, named files. `_rel` companions when `working-directory:` is used.
248+
- **Docker stages**: each `FROM` stage declares `ENV PLATFORM_BUILD_DIR=...` and `ENV FINAL_DIR=...` once. Subsequent RUN steps reference the variables.
249+
250+
The first repo (socket-btm) is the worked example. Read its `scripts/paths.mts` files and `.github/workflows/*.yml` for canonical patterns when applying the strategy elsewhere.

0 commit comments

Comments
 (0)