# CLAUDE.md

**MANDATORY**: Act as principal-level engineer. Follow these guidelines exactly.

<!-- BEGIN FLEET-CANONICAL — sync via socket-wheelhouse/scripts/sync-scaffolding.mts. Do not edit downstream. -->

## 📚 Wheelhouse Standards

### Identifying users

Identify users by git credentials and use their actual name. Use "you/your" when speaking directly; use names when referencing contributions (enforced by `.claude/hooks/identifying-users-reminder/`).

### Parallel Claude sessions

🚨 Multiple Claude sessions may target the same checkout (parallel agents, terminals, or worktrees on the same `.git/`). **The umbrella rule:** never run a git command that mutates state belonging to a path other than the file you just edited. Forbidden in the primary checkout: `git stash`, `git add -A` / `git add .` (enforced by `.claude/hooks/overeager-staging-guard/`; bypass: `Allow add-all bypass`), `git checkout/switch <branch>`, `git reset --hard <non-HEAD>`. Branch work goes in a `git worktree`. Cross-repo imports via `@socketsecurity/lib/...` only, never `../<sibling-repo>/...` (enforced by `.claude/hooks/cross-repo-guard/`). Full prohibition list + worktree recipe in [`docs/claude.md/fleet/parallel-claude-sessions.md`](docs/claude.md/fleet/parallel-claude-sessions.md).

### Default branch fallback

Never hard-code `main` in scripts — a few legacy repos still use `master`. Resolve via `git symbolic-ref refs/remotes/origin/HEAD`, fall back to `main` then `master`:

```bash
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
[ -z "$BASE" ] && git show-ref --verify --quiet refs/remotes/origin/main && BASE=main
[ -z "$BASE" ] && git show-ref --verify --quiet refs/remotes/origin/master && BASE=master
BASE="${BASE:-main}"
```

Apply in: worktree creation, base-ref resolution for `git diff`/`git rev-list`, PR base detection, hook scripts walking history. Doc examples may write `main` for clarity; scripts must look up. Order matters — `main → master` matches fleet reality; reversing would mispick during rename migrations (enforced by `.claude/hooks/default-branch-guard/`).

### Public-surface hygiene

🚨 The rules apply even when hooks are not installed (enforced by `.claude/hooks/{private-name-guard,public-surface-reminder,release-workflow-guard}/`):

- **Real customer / company names** — never write one into a commit, PR, issue, comment, or release note. Replace with `Acme Inc` or rewrite the sentence to not need the reference. (No enumerated denylist exists — a denylist is itself a leak.)
- **Private repos / internal project names** — never mention. Omit the reference entirely; don't substitute "an internal tool" — the placeholder is a tell.
- **Linear refs** — never put `SOC-123`/`ENG-456`/Linear URLs in code, comments, or PR text. Linear lives in Linear.
- **Publish / release / build-release workflows** — never `gh workflow run|dispatch`. The user runs them manually. Bypass: either `gh workflow run -f dry-run=true` (workflow must declare `dry-run:` input, no force-prod override set) OR `Allow workflow-dispatch bypass: <workflow>` typed verbatim — one phrase authorizes one dispatch. `workflow_dispatch.inputs` keys are kebab-case (`dry-run`, `build-mode`); snake_case silently fails the bypass.
- **Workflow YAML rules** — `uses: <action>@<40-char-sha>` lines need a trailing `# <tag> (YYYY-MM-DD)` comment (enforced by `.claude/hooks/workflow-uses-comment-guard/`). Workflow `run:` blocks with `gh ... --body "..."` break YAML on multi-line markdown; always `--body-file <path>` (enforced by `.claude/hooks/workflow-yaml-multiline-body-guard/`; bypass: `Allow workflow-yaml-multiline-body bypass`). Edits to `.github/workflows/*.y*ml` auto-lint via local `actionlint` (enforced by `.claude/hooks/actionlint-on-workflow-edit/`).
- **`pull_request_target` is privileged** — runs in BASE-repo context with secrets. Never combine it with `actions/checkout` of fork head + a step that executes the checked-out code (enforced by `.claude/hooks/pull-request-target-guard/`). Full threat model + safer patterns in [`docs/claude.md/fleet/pull-request-target.md`](docs/claude.md/fleet/pull-request-target.md).
- **No external issue/PR refs in commit messages or PR bodies.** GitHub auto-links `<owner>/<repo>#<num>` and `https://github.com/<owner>/<repo>/(issues|pull)/<num>` mentions back to the target issue, spamming the maintainer with `added N commits that reference this issue` events. Only SocketDev-owned refs are allowed (`SocketDev/<repo>#<num>` is fine). For upstream maintainer issues, link them in _the PR description prose_ (which doesn't trigger backrefs from commits) or use `[#1203](https://npmx.dev/...)` link form that omits the `owner/repo#` token. Bypass: `Allow external-issue-ref bypass` (enforced by `.claude/hooks/no-external-issue-ref-guard/`).

### Canonical README

🚨 Root `README.md` follows the fleet skeleton — 5 level-2 sections in order (Why this repo exists / Install / Usage / Development / License), no `socket-wheelhouse` mentions (it's a private repo), no sibling-relative script commands (e.g. `node ../socket-foo/scripts/...` fails for outside readers). Canonical skeleton: `socket-wheelhouse/template/README.md`. Bypass: `Allow readme-fleet-shape bypass` (enforced by `.claude/hooks/readme-fleet-shape-guard/`).

### Commits & PRs

- Conventional Commits `<type>(<scope>): <description>` — NO AI attribution.
- **When adding commits to an OPEN PR**, update the PR title and description to match the new scope. Use `gh pr edit <num> --title … --body …`. The reviewer should know what's in the PR without scrolling commits.
- **Replying to Cursor Bugbot** — reply on the inline review-comment thread, not as a detached PR comment: `gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies -X POST -f body=…`.
- **Backing out an unpushed commit** — prefer `git reset --soft HEAD~1` (or `git rebase -i HEAD~N`) over `git revert`. Revert commits are for changes already on origin; for local-only commits they just pollute history (enforced by `.claude/hooks/prefer-rebase-over-revert-guard/`).
- **No empty commits.** Never use `git commit --allow-empty`, and never use `git cherry-pick --allow-empty` / `--keep-redundant-commits` to replay a no-content commit forward. Empty commits exist almost exclusively as "anchor" tags for releases, but the right place to anchor a release is on the actual commit that bumped `package.json` + `CHANGELOG.md` — moving the tag forward (`git tag -f vX.Y.Z`) is the correct mechanism. Empty commits pollute `git log` and break tooling that expects commits to have content (CHANGELOG generators, `git log -p`, blame). Bypass: `Allow empty-commit bypass` (enforced by `.claude/hooks/no-empty-commit-guard/`).
- **Commit author** — every commit must use the user's canonical GitHub identity, not a work email or a substituted name. Canonical lives in `~/.claude/git-authors.json` (or global git config); aliases in `aliases[]` are also accepted (enforced by `.claude/hooks/commit-author-guard/`).
- **No AI attribution in drafts either** — when drafting a commit body or PR description, omit "Generated with Claude", "Co-Authored-By: Claude", and robot-emoji-tagged lines (enforced by `.claude/hooks/commit-pr-reminder/`).
- **Push policy: push, fall back to PR.** Default to `git push origin <branch>` on the current branch (typically `main`). If the push is rejected — branch protection requires a PR, conflicts, signature/identity rejection — open a PR via `gh pr create` against the default base. Don't pre-open PRs "to be safe"; the direct-push happy path is faster for the operator. Don't force-push to recover; resolve the actual cause (rebase to fix conflicts, fix the commit identity, etc.). Reminder fires when `gh pr create` is invoked without an explicit user directive ("PR this", "open a PR") (enforced by `.claude/hooks/pr-vs-push-default-reminder/`).

### Version bumps

🚨 When the user asks for a version bump (`bump to vX.Y.Z`, `tag X.Y.Z`, `release X`, etc.), the sequence is exactly: (1) pre-bump prep wave `pnpm run update` → `pnpm i` → `pnpm run fix --all` → `pnpm run check --all` (each must finish clean); (2) CHANGELOG entry, **public-facing only** — new exports / signature changes / migration recipes, NOT internal refactors or `chore(sync)` cascades; (3) `chore: bump version to X.Y.Z` is the LAST commit on the release; (4) `git tag vX.Y.Z` at that commit (enforced by `.claude/hooks/version-bump-order-guard/`); (5) do NOT dispatch the publish workflow — user-triggered. Full sequence + rationale in [`docs/claude.md/fleet/version-bumps.md`](docs/claude.md/fleet/version-bumps.md).

### Programmatic Claude calls

🚨 Workflows / skills / scripts that invoke `claude` CLI or `@anthropic-ai/claude-agent-sdk` MUST set all four lockdown flags: `tools`, `allowedTools`, `disallowedTools`, `permissionMode: 'dontAsk'`. Never `default` mode in headless contexts. Never `bypassPermissions`. See `.claude/skills/locking-down-programmatic-claude/SKILL.md`.

### Tooling

- **Package manager**: `pnpm`. Run scripts via `pnpm run foo --flag`, never `foo:bar`. After `package.json` edits, `pnpm install`.
- 🚨 NEVER use `npx`, `pnpm dlx`, or `yarn dlx` — use `pnpm exec <package>` or `pnpm run <script>` # socket-hook: allow npx
- 🚨 NEVER pass `--experimental-strip-types` to Node (enforced by `.claude/hooks/no-experimental-strip-types-guard/`).
- **New dependencies** — every new dep added to `package.json` runs a Socket-score check at edit time; low-scoring deps block (enforced by `.claude/hooks/check-new-deps/`). The 7-day `minimumReleaseAge` soak is intentional malware protection; never add to `pnpm-workspace.yaml` `minimumReleaseAge.exclude[]` (bypass `Allow minimumReleaseAge bypass` for emergency CVE patches; enforced by `.claude/hooks/minimum-release-age-guard/`). Every per-package soak-bypass entry (the `'pkg@1.2.3'` exact-pin form) MUST carry a `# published: YYYY-MM-DD | removable: YYYY-MM-DD` annotation as the LAST comment line above the bullet — `published` is the version's npm publish date, `removable` is `published + 7d` so a periodic cleanup can drop entries that no longer need the bypass (enforced by `.claude/hooks/soak-exclude-date-annotation-guard/` at edit time + `scripts/check-soak-exclude-dates.mts` at commit time). Vitest `include` globs must not match `node:test` files — mismatched runners produce confusing "no test suite found" errors (enforced by `.claude/hooks/vitest-include-vs-node-test-guard/`).
- **Bundler**: `rolldown`, NOT `esbuild`. The fleet standardizes on rolldown for direct bundling (see `template/.config/rolldown/`). Transitive esbuild deps (e.g. via vitest) are unavoidable today — the rule is no _new direct_ esbuild use anywhere in the fleet.
- **Backward compatibility** — FORBIDDEN to maintain. Actively remove when encountered.
- Full ruleset (packageManager field, `.config/` placement, `.mts` runners, soak time, shallow submodules, monorepo `engines.node`, `npm-run-all2` + `node --run` opt-in) in [`docs/claude.md/fleet/tooling.md`](docs/claude.md/fleet/tooling.md).

### Claude Code plugin pins

🚨 Fleet-blessed Claude Code plugins are SHA-pinned in the wheelhouse-canonical [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json), with companion human-readable metadata (pin date, pinner) in [`.claude-plugin/README.md`](../.claude-plugin/README.md). The pair is enforced together: every `plugins[].source.sha` in `marketplace.json` must have a row in the README table with matching `version` + `sha` + an ISO-8601 `date`. Same staleness signal the GHA `uses:` SHA-pin comments carry. Bump the SHA → bump the row. Run `pnpm run install-claude-plugins` to reconcile a machine to the pinned set — adds the marketplace + installs each plugin at its pinned SHA, no plugin modifications (enforced by `.claude/hooks/marketplace-comment-guard/`).

### Token minification

Two surfaces apply lossless compression to Claude tool_result payloads — `minify` (JSON whitespace), `strip-lines` (`cat -n` prefixes), `whitespace` (3+ blank lines → 1). All deterministic and information-preserving; no semantic ML compression. **Wire-level proxy**: `@socketsecurity/token-minifier` in [`socket-wheelhouse/packages/`](../packages/socket-token-minifier/) sits between Claude Code and `api.anthropic.com` when `ANTHROPIC_BASE_URL=http://localhost:7779` is set. Installed via `pnpm run install-token-minifier` (self-contained at `~/.socket/_wheelhouse/socket-token-minifier/`, shim at `~/.socket/_wheelhouse/bin/socket-token-minifier`). Auto-started by `.claude/hooks/socket-token-minifier-start/` SessionStart hook — **fail-closed**: only writes `ANTHROPIC_BASE_URL` to the session env if the proxy is verified healthy on `:7779`; if not, session goes direct to api.anthropic.com. **In-context hook**: [`.claude/hooks/minify-mcp-output/`](.claude/hooks/minify-mcp-output/) fires PostToolUse on MCP-tool results and returns `hookSpecificOutput.updatedMCPToolOutput` — the only documented rewrite channel for already-collected tool outputs (built-in tools like Read/Bash have no such channel; use the proxy for those) (enforced by `.claude/hooks/minify-mcp-output/`, `.claude/hooks/socket-token-minifier-start/`).

### Fix it, don't defer

🚨 See a lint/type/test error or broken comment in your reading window — fix it. Stop current task, fix the issue in a sibling commit, resume. Don't label as "pre-existing", "unrelated", or "out of scope" — the labels are rationalizations (enforced by `.claude/hooks/excuse-detector/`).

🚨 Don't blame the user (or "the linter") when your own edits get reverted between turns. The cause is almost always your own scripts: pre-commit autofix, sync-cascade from `template/`, oxlint --fix. Investigate with `git log -S`, run pre-commit phases in isolation, diff `template/` canonical sources. Only attribute to the user with direct evidence (enforced by `.claude/hooks/dont-blame-user-reminder/`).

🚨 Never offer "fix vs accept-as-gap" as a choice — pick the fix.

Exceptions (state the trade-off and ask): genuinely large refactor on a small bug, file belongs to another session, fix needs off-machine action.

### Don't leave the worktree dirty

🚨 When you finish a code change, **commit it**. Don't end a turn with uncommitted edits, untracked new files, or staged-but-uncommitted hunks lingering in the working tree. A dirty worktree is a half-finished job: another session, another agent, or a future `git checkout` will trip over it, and the user has to clean up after you.

Rules:

- **After finishing a logical unit of work, commit it.** Use a Conventional Commits message per the _Commits & PRs_ rule. Never leave the working tree dirty between turns.
- **Surgical staging only** — `git add <specific-file>`, never `-A` / `.` (per the _Parallel Claude sessions_ rule). The dirty-worktree rule is no excuse to sweep in files you didn't touch. `git add -f` is forbidden for paths containing `/node_modules/` or `package-lock.json` under `.claude/hooks/*/` or `.claude/skills/*/` — past incident: cascading agent ran `git add -f` of node_modules into 6 fleet repos, force-push-only to recover (enforced by `.claude/hooks/node-modules-staging-guard/`; bypass: `Allow node-modules-staging bypass`).
- **Stage only when you're about to commit.** `git add` and `git commit` belong on the same line (chained with `&&`) OR in the same Bash call. Don't stage as a side-effect of "preparing" — staging is a commit-time action. A turn that ends with staged-but-uncommitted hunks is the failure mode the previous bullet warns against (enforced by `.claude/hooks/no-orphaned-staging/`).
- **If you genuinely can't commit yet** (the change is mid-refactor, tests are failing, you're waiting on user input), say so explicitly in the turn summary so the user knows the dirty state is intentional. Silent dirty worktrees are the failure mode.
- **Worktrees from `git worktree add`** — same rule, sharper: a transient task-worktree must be left clean (committed + pushed) before `git worktree remove`, or the removal refuses and you've stranded the work.

The principle: the working tree at end-of-turn should match the user's mental model of where the work is. "Done" means committed; anything else is paused, and pause states need to be announced.

### Smallest chunks, land ASAP

🚨 Smallest possible chunks; land ASAP via direct-push-to-main. Don't accumulate work across worktrees or long-lived branches — each unmerged branch is in-flight state that has to be rebased and reconciled later. Same instinct that flags _Drift watch_ across fleet repos applies to in-flight branches in one repo. Past incident: 4 sibling wheelhouse worktrees (2 dead, 2 needing rebase) burned a turn on consolidation. **How to apply:** finish a branch the session it's opened; consolidate any pile-up at session start before resuming the queue.

### Untracked-by-default for vendored / build-copied trees

🚨 Untracked dirs under `additions/source-patched/`, `vendor/`, `third_party/`, `external/`, `upstream/`, `deps/<libname>/`, `pkg-node/`, or `*-bundled`/`*-vendored` paths are **untracked-by-default**. Before staging: `git status --ignored` + read `.gitignore` (look for `dir/*` + `!dir/file` allowlists — the allowlisted file is our hand-written glue, not the whole tree) + grep for the build script that copies the dir in. When REMOVING a class / attribute / selector that other code consumes, grep BOTH the repo root AND every `upstream/` / `vendor/` / `third_party/` submodule before deleting — past incident: stripped a CSS class because repo-root grep found 0 hits; upstream bundle hydrated from it and the rendered output went blank (enforced by `.claude/hooks/consumer-grep-reminder/`). Ban "must be" / "presumably" / "looks like" when handling someone else's tree — run the command instead. Ask before committing 100+ file or multi-MB drops. Full playbook in [`docs/claude.md/fleet/untracked-by-default.md`](docs/claude.md/fleet/untracked-by-default.md).

### Hook bypasses require the canonical phrase

🚨 Reverting tracked changes or bypassing a hook (--no-verify, DISABLE*PRECOMMIT*\*, --no-gpg-sign, force-push) requires the user to type **`Allow <X> bypass`** verbatim in a recent user turn (e.g. `Allow revert bypass`, `Allow no-verify bypass`). Paraphrases don't count (enforced by `.claude/hooks/no-revert-guard/`). Full phrase table: [`docs/claude.md/fleet/bypass-phrases.md`](docs/claude.md/fleet/bypass-phrases.md).

**Exception — fleet-sync cascade.** Mechanical `chore(sync): cascade fleet template@<sha>` operations across the fleet would otherwise need a fresh bypass phrase per repo. Prefix cascade Bash commands with `FLEET_SYNC=1` to opt in: the sentinel allowlists exactly three operations — (1) `git commit --no-verify` whose message starts with `chore(sync): cascade fleet template@`; (2) `git push --no-verify`; (3) broad-stage `git add -A` / `git add -u` / `git add .` (safe inside a fresh worktree off `origin/main`, which is how cascade scripts work). Everything else with `FLEET_SYNC=1` still falls through to the normal checks — `git stash`, `git reset --hard`, `git checkout/restore`, non-cascade commits all still need the canonical phrase. The sentinel is opt-in per command; no global env-var poisoning. (Enforced by `.claude/hooks/no-revert-guard/` + `.claude/hooks/overeager-staging-guard/`.)

### Variant analysis on every High/Critical finding

🚨 When a finding lands at severity High or Critical, **search the rest of the repo for the same shape** before closing it. Bugs cluster — same mental model, same antipattern. Three searches: same file (read the whole thing, not just the hunk), sibling files (`rg` the shape, not the names), cross-package (parallel implementations love to drift).

Skip for style nits. Full taxonomy in [`.claude/skills/_shared/variant-analysis.md`](.claude/skills/_shared/variant-analysis.md). Cross-fleet variants become a _Drift watch_ task — open `chore(sync): cascade <fix>` (enforced by `.claude/hooks/variant-analysis-reminder/`).

### Compound lessons into rules

When the same kind of finding fires twice — across two runs, two PRs, or two fleet repos — **promote it to a rule** instead of fixing it again. Land it in CLAUDE.md, a `.claude/hooks/*` block, or a skill prompt — pick the lowest-friction surface. Always cite the original incident in a `**Why:**` line. Skip the retrospective doc; the rule is the artifact (enforced by `.claude/hooks/compound-lessons-reminder/`). Discipline: [`.claude/skills/_shared/compound-lessons.md`](.claude/skills/_shared/compound-lessons.md).

Every new `.claude/hooks/<name>/` hook must have a matching `(enforced by `.claude/hooks/<name>/`)` reference in CLAUDE.md before the hook's `index.mts` can be written (enforced by `.claude/hooks/new-hook-claude-md-guard/`). Hooks ignore CLAUDE.md themselves — citing the enforcer inline keeps the rule visible to whoever's reading either surface.

### Plan review before approval

For non-trivial work (multi-file refactor, new feature, migration), the plan itself is a deliverable. List steps numerically, name files you'll touch, name rules you'll honor — don't bury the plan in prose. If the plan touches fleet-shared resources (this CLAUDE.md fleet block, hooks, `_shared/`), invite a second-opinion pass before writing code. If the plan adds a fleet rule, name the original incident (per _Compound lessons_) (enforced by `.claude/hooks/plan-review-reminder/`).

### Plan storage

🚨 Design / implementation / migration plan docs live at `<repo-root>/.claude/plans/<lowercase-hyphenated>.md` and are **never tracked by version control** — the fleet `.gitignore` excludes `/.claude/*` and `plans/` is intentionally absent from the allowlist. Don't write plans into `docs/plans/` or a package-level `<pkg>/docs/plans/` (enforced by `.claude/hooks/plan-location-guard/`; bypass: `Allow plan-location bypass`). Full rationale + migration guidance in [`docs/claude.md/fleet/plan-storage.md`](docs/claude.md/fleet/plan-storage.md).

### Drift watch

🚨 **Drift across fleet repos is a defect, not a feature.** When two socket-\* repos pin different versions of the same shared resource (a tool in `external-tools.json`, a workflow SHA, a CLAUDE.md fleet block, a hook in `.claude/hooks/`, an upstream submodule, `.gitmodules` `# name-version` annotations enforced by `.claude/hooks/gitmodules-comment-guard/`, pnpm/Node `packageManager`/`engines`), **opt for the latest**. Canonical sources: `socket-registry`'s `setup-and-install` action for tool SHAs; `socket-wheelhouse`'s `template/` tree for `.claude/`, CLAUDE.md fleet block, hooks. Either reconcile in the same PR or open `chore(sync): cascade <thing> from <newer-repo>` and link it (enforced by `.claude/hooks/drift-check-reminder/`). Full drift-surface list + cascade-PR convention in [`docs/claude.md/fleet/drift-watch.md`](docs/claude.md/fleet/drift-watch.md).

### Stranded cascades

🚨 Local-only `chore(sync): cascade fleet template@<sha>` commits + `chore/sync-<sha>` worktrees whose template SHA has been superseded on origin accumulate from interrupted cascade waves and silently block future pushes. The wheelhouse cascade auto-runs `socket-wheelhouse/scripts/cascade-tooling/cleanup-stranded.mts --target <repo>` at the start of every wave (default = fix; pass `--dry-run` to report only). Safety rails: cascade-subject regex match + trusted commit author + strict-ancestor proof of supersession + cascade-allowlist file check. Any ambiguity → bail the whole repo. Full algorithm + recovery instructions in [`docs/claude.md/fleet/stranded-cascades.md`](docs/claude.md/fleet/stranded-cascades.md).

### Never fork fleet-canonical files locally

🚨 Edit fleet-canonical files (anything in the sync manifest) ONLY in `socket-wheelhouse/template/...` — never in a downstream repo. Spot a missing helper in a downstream copy? Lift it upstream and re-cascade (enforced by `.claude/hooks/no-fleet-fork-guard/`; bypass: `Allow fleet-fork bypass`). Full canonical-surface list + lifting workflow: [`docs/claude.md/wheelhouse/no-local-fork-canonical.md`](docs/claude.md/wheelhouse/no-local-fork-canonical.md).

### Code style

Default to no comments (enforced by `.claude/hooks/no-meta-comments-guard/`); when written, write for a junior reader. Heaviest fleet invariants: no `TODO`/`FIXME`/stubs; `undefined` over `null`; `httpJson`/`httpText` from `@socketsecurity/lib/http-request` over `fetch()`; `safeDelete()` from `@socketsecurity/lib/fs` over `fs.rm`; Edit tool over `sed`/`awk`; `JSON.parse(JSON.stringify(x))` over `structuredClone(x)` for JSON-shaped data (enforced by `.claude/hooks/no-structured-clone-prefer-json-guard/` + `socket/no-structured-clone-prefer-json` oxlint rule; bypass: `Allow no-structured-clone-prefer-json bypass`); `getDefaultLogger()` over `console.*` (enforced by `.claude/hooks/logger-guard/`). Cross-port files use `Lock-step` comments — see [`docs/claude.md/fleet/parser-comments.md`](docs/claude.md/fleet/parser-comments.md) §5–7 (enforced by `.claude/hooks/lock-step-ref-guard/` + `scripts/check-lock-step-{refs,header}.mts`; bypass: `Allow lock-step bypass`). Full ruleset (object literals, imports, subprocesses, file existence, env checks, generated reports, sorting, Promise.race, Safe suffix, `node:smol-*`, doc filenames, inline-defer, ESLint-config refs, inclusive language) in [`docs/claude.md/fleet/code-style.md`](docs/claude.md/fleet/code-style.md). See also [`docs/claude.md/fleet/sorting.md`](docs/claude.md/fleet/sorting.md) and [`docs/claude.md/fleet/inclusive-language.md`](docs/claude.md/fleet/inclusive-language.md).

### No underscore-prefixed identifiers

🚨 Never prefix an **identifier** (function, variable, type, export) with `_` — patterns like `_resetX`, `_cache`, `_doFoo`, `_internal` are banned at the symbol level. Privacy in TS is handled by module boundaries (not exporting) or by `_internal/` _directory_ layout; the underscore-as-internal-marker convention from other languages adds noise without enforcement. Exporting "internal" helpers is fine and explicitly preferred — easier to unit-test. **Exception:** the directory name `_internal/` is allowed (and is the documented way to signal module-private files); the rule is about identifiers inside files, not folder layout (enforced by `.claude/hooks/no-underscore-identifier-guard/` + the `socket/no-underscore-identifier` oxlint rule; bypass: `Allow underscore-identifier bypass`).

### File size

Soft cap **500 lines**, hard cap **1000 lines** per source file. Past those, split along natural seams — group by domain, not line count; name files for what's in them; co-locate helpers with consumers. Exceptions: a single function that legitimately needs the space (note it inline), or a generated artifact. Full playbook in [`docs/claude.md/fleet/file-size.md`](docs/claude.md/fleet/file-size.md).

### Lint rules: errors over warnings, fixable over reporting

- **Errors, not warnings.** Default `"error"` for new rules.
- **Fixable when possible.** Ship an autofix (`fixable: 'code'` + `fix(fixer) => ...`) whenever the rewrite is deterministic.
- **Skill or hook ≠ no rule.** Defense in depth — skill is docs, hook is edit-time, lint is commit-time.
- **Tooling: oxlint + oxfmt only.** No ESLint, no Prettier. Fleet socket-\* oxlint plugin lives in `template/.config/oxlint-plugin/`.
- **Invoke oxfmt / oxlint with `-c .config/...rc.json` explicitly.** Both tools accept a `-c PATH` (oxfmt) / `--config PATH` (oxlint). The fleet keeps both configs under `.config/`, not at repo root. Without the flag, the tools fall through to their built-in defaults — oxfmt's default is double-quotes + semis, the opposite of the fleet style, and would silently rewrite ~200 files on `pnpm run format`. Canonical script bodies in `manifest.mts` already encode the flag; the sync-scaffolding gate rewrites drifted scripts back to the canonical form.
- **No file-scope `oxlint-disable`.** Always use `oxlint-disable-next-line <rule> -- <reason>` per call site so each exemption is independently justified in `git blame`. File-scope blocks silently exempt future edits the author never thought about (enforced by `socket/no-file-scope-oxlint-disable` lint rule + `.claude/hooks/no-file-scope-oxlint-disable-guard/` edit-time guard).

Full rationale + cascade behavior in [`docs/claude.md/fleet/lint-rules.md`](docs/claude.md/fleet/lint-rules.md).

### 1 path, 1 reference

A path is constructed exactly once. Everywhere else references the constructed value.

- **Within a package**: every script imports its own `scripts/paths.mts`. No `path.join('build', mode, …)` outside that module. `paths.mts` is per-package (like `package.json`) — every package that has a `scripts/` dir has its own.
- **Across packages**: package B imports package A's `paths.mts` via the workspace `exports` field. Never `path.join(PKG, '..', '<sibling>', 'build', …)`.
- **Sub-packages inherit**: a sub-package's `paths.mts` `export * from '<rel>/paths.mts'` from the nearest ancestor and adds local overrides below the re-export. Don't re-derive `REPO_ROOT` / `CONFIG_DIR` / `NODE_MODULES_CACHE_DIR` (enforced by `.claude/hooks/paths-mts-inherit-guard/`).
- **Not just build paths**: `paths.mts` is for _every_ path the package constructs — config files (`socket-wheelhouse.json`), lockfiles, cache dirs, manifest files. The fleet ships a starter `template/scripts/paths.mts` that exports the common constants + `loadSocketWheelhouseConfig()`.
- **Workflows / Dockerfiles / shell** can't `import` TS — construct once, reference by output / `ENV` / variable.
- **Canonical layout**: build outputs live at `<package-root>/build/<mode>/<platform-arch>/out/Final/<artifact>`, where `mode ∈ {dev, prod}` and `platform-arch` is the Node-style `<process.platform>-<process.arch>` (e.g. `darwin-arm64`, `linux-x64`). socket-btm is the worked example; ultrathink follows it; smaller TS-only repos that don't fork by platform may use `'any'` as the platform-arch sentinel but keep the same nesting. Each package's `scripts/paths.mts` exports `PACKAGE_ROOT`, `BUILD_ROOT`, and `getBuildPaths(mode, platformArch)` returning at minimum `outputFinalDir` + `outputFinalFile`/`outputFinalBinary`.

Three-level enforcement: `.claude/hooks/path-guard/` blocks build-path construction outside `paths.mts` at edit time; `.claude/hooks/paths-mts-inherit-guard/` blocks sub-package `paths.mts` files that don't inherit from the nearest ancestor; `scripts/check-paths.mts` is the whole-repo gate run by `pnpm check`; `/guarding-paths` is the audit-and-fix skill. Find the canonical owner and import from it.

### Cross-platform path matching

When a regex matches against a path string, **normalize the path first** with `normalizePath` (or `toUnixPath`) from `@socketsecurity/lib/paths/normalize` and write the regex against `/` only. Don't write dual-separator patterns like `[/\\]` — they're easy to miss in some branches, slower to read, and they multiply when you add `\\\\` for escaped Windows separators. `normalizePath` is the same helper the fleet uses everywhere; relying on it gives one path representation across `darwin` / `linux` / `win32` (enforced by `.claude/hooks/path-regex-normalize-reminder/`). Bypass: `Allow path-regex-normalize bypass`.

### Background Bash

Never use `Bash(run_in_background: true)` for test / build commands (`vitest`, `pnpm test`, `pnpm build`, `tsgo`). Backgrounded runs you don't poll get abandoned and leak Node workers. Background mode is for dev servers and long migrations whose results you'll consume. If a run hangs, kill it: `pkill -f "vitest/dist/workers"`. The `.claude/hooks/stale-process-sweeper/` `Stop` hook reaps true orphans as a safety net.

When writing or extending a Bash-allowlist hook, prefer **AST-based parsing** over regex matchers when the rule needs to reason about command structure (chains, subshells, redirects, command substitution). Regex matchers approve `git $(echo rm) foo.txt` because the surface looks like `git`; an AST parser sees the substitution and blocks. Pure-syntactic rules (binary name only) can stay regex; structure-sensitive rules (no writes to `.env*`, no destructive chains, no `$(…)` containing destructive verbs) need a parser. Pattern reference: https://github.com/ldayton/Dippy.

### Judgment & self-evaluation

- If the request is based on a misconception, say so before executing.
- If you spot an adjacent bug, flag it: "I also noticed X — want me to fix it?"
- Fix warnings (lint / type / build / runtime) when you see them — don't leave them for later. For UI/render changes (`*.html` / `*.css` / `scripts/tour.mts`-shape files): rebuild the artifact + verify the rendered output BEFORE committing — past pattern: multiple wasted commits per session ("rebuild before you fucking commit") (enforced by `.claude/hooks/verify-rendered-output-before-commit-reminder/`).
- **Default to perfectionist** when you have latitude. "Works now" ≠ "right." Don't offer "do it right" vs "ship fast" as a binary choice menu — pick perfectionist and execute (enforced by `.claude/hooks/perfectionist-reminder/`).
- Before calling done: perfectionist vs. pragmatist views. Default perfectionist absent a signal.
- If a fix fails twice: stop, re-read top-down, state where the mental model was wrong, try something fundamentally different.
- **When the user authorizes a queue** ("complete each one", "hammer it out", "100%", "do them all"): finish every item before stopping. Don't post "what's next?" / "honest stopping point" / "session totals" after one item — that re-litigates intent already given. Continue until the queue is empty or a genuine blocker hits (enforced by `.claude/hooks/dont-stop-mid-queue-reminder/`). Skip AskUserQuestion when recent transcript carries explicit go-ahead directives ("do it" / "yes" / "proceed") — pick the obvious default and execute (enforced by `.claude/hooks/ask-suppression-reminder/`).

### Error messages

An error message is UI. The reader should fix the problem from the message alone. Four ingredients in order:

1. **What** — the rule, not the fallout (`must be lowercase`, not `invalid`).
2. **Where** — exact file / line / key / field / flag.
3. **Saw vs. wanted** — the bad value and the allowed shape or set.
4. **Fix** — one imperative action (`rename the key to …`).

Use `isError` / `isErrnoException` / `errorMessage` / `errorStack` from `@socketsecurity/lib/errors` over hand-rolled checks. Use `joinAnd` / `joinOr` from `@socketsecurity/lib/arrays` for allowed-set lists. Vague-shape `throw new Error("…")` strings are flagged on Stop (enforced by `.claude/hooks/error-message-quality-reminder/`). Full guidance in [`docs/claude.md/fleet/error-messages.md`](docs/claude.md/fleet/error-messages.md).

### Token hygiene

🚨 Never emit the raw value of any secret to tool output, commits, comments, or replies; when blocked, rewrite — don't bypass. Redact `token` / `jwt` / `api_key` / `secret` / `password` / `authorization` fields when citing API responses (enforced by `.claude/hooks/token-guard/`). Long-lived CLI logins are auto-rotated to limit stale-token exposure (enforced by `.claude/hooks/auth-rotation-reminder/`).

**Tokens belong in env vars (CI) or the OS keychain (dev local) — nowhere else.** Never in `.env` / `.env.local` / `.envrc` / `~/.sfw.config` / `~/.config/socket/*` / any dotfile. Dotfiles leak via accidental commits, file-indexers, backup clients, shell-history dumps. Initial setup: `node .claude/hooks/setup-security-tools/install.mts` (prompts + persists via macOS Keychain / Linux libsecret / Windows CredentialManager). **Rotation: `node .claude/hooks/setup-security-tools/install.mts --rotate`** — TTY-muted prompt, overwrites the keychain entry unconditionally, ignores stale dotfile / env-var lookup. This is the ONLY correct rotator. Suggesting any other path (`socket login`, hand-editing `~/.sfw.config`, `export SOCKET_API_TOKEN=…` in a shell rc) is a token-hygiene violation. The Stop-hook flags broken sfw shims, free-vs-enterprise edition drift, and 401-rejection patterns from the last assistant turn (enforced by `.claude/hooks/setup-security-tools/`, `.claude/hooks/no-token-in-dotenv-guard/`).

🚨 **Never call platform keychain CLIs directly from Bash to read a token.** `security find-generic-password` (macOS), `secret-tool lookup` (Linux), `Get-StoredCredential` (Windows PowerShell), `keyring get` (cross-platform) all surface a UI auth prompt on the user's screen — and that prompt fires _per call_, so a hook chain that reads the keychain three times costs three prompts. The token is already cached in process memory after the first resolution (see [`api-token.mts`](.claude/hooks/setup-security-tools/lib/api-token.mts) module-scope cache); read it from `findApiToken()` or `process.env.SOCKET_API_KEY` / `SOCKET_API_TOKEN` instead. Writes (`security add-generic-password`, `secret-tool store`, `New-StoredCredential`) and deletes are allowed — they happen during operator-driven setup / rotation, never on hot paths. Bypass: `Allow blind-keychain-read bypass` (enforced by `.claude/hooks/no-blind-keychain-read-guard/`).

**Socket API token env var** — fleet docs / workflow inputs / `.env.example` use the forward-canonical name `SOCKET_API_TOKEN`. For local-dev (keychain + `~/.zshenv`) the install hook stores under and exports `SOCKET_API_KEY` because it's universally supported across every Socket tool (CLI, SDK, sfw, fleet scripts) — one env var, no fallback chain. `SOCKET_API_TOKEN` is accepted as a secondary read; `SOCKET_SECURITY_API_TOKEN` / `SOCKET_SECURITY_API_KEY` remain accepted aliases for one cycle. Don't confuse any of these with `SOCKET_CLI_API_TOKEN` (socket-cli's separate setting).

Full spec (hook details, personal-path placeholders, cross-repo path references) in [`docs/claude.md/fleet/token-hygiene.md`](docs/claude.md/fleet/token-hygiene.md).

### Agents & skills

- `/scanning-security` — AgentShield + zizmor audit
- `/scanning-quality` — quality analysis
- Shared subskills in `.claude/skills/_shared/`
- **Handing off to another agent** — see [`docs/claude.md/fleet/agent-delegation.md`](docs/claude.md/fleet/agent-delegation.md).
- **Skill scope tiers** (fleet / partial / unique), the `updating` umbrella + `updating-*` siblings convention, and the `scripts/run-skill-fleet.mts` cross-fleet runner in [`docs/claude.md/fleet/agents-and-skills.md`](docs/claude.md/fleet/agents-and-skills.md).

### Tool-specific guards

Hooks that gate specific external tools — they only fire when those tools appear in a command, so they're safe to wire fleet-wide:

- `codex-no-write-guard` — blocks `codex` CLI / `codex:codex-rescue` Agent invocations with write-intent flags or prompts. The rule (originally from ultrathink: Codex regressions cost real perf; use Codex for advice not code changes) applies fleet-wide whenever Codex is invoked. Bypass: `Allow codex-write bypass` (enforced by `.claude/hooks/codex-no-write-guard/`).
- `concurrent-cargo-build-guard` — blocks a second `cargo build --release` while one is in flight (8 LLVM threads × 8-22GB = OOM on dual builds). Fires only on cargo release commands, so a no-op in non-cargo repos. Bypass: `Allow concurrent-cargo-build bypass` (enforced by `.claude/hooks/concurrent-cargo-build-guard/`).

<!-- END FLEET-CANONICAL -->

## 🏗️ Project-Specific

socket-mcp is the Socket Model Context Protocol server — exposes Socket dependency scanning + Socket.dev API surfaces to MCP-aware clients (Claude Desktop, Cursor, etc.).

### Layout

- `index.ts` — entry; registers MCP tools and starts the stdio server.
- `lib/` — tool implementations and Socket API wrappers.
- `artifacts.test.ts` — co-located unit tests; additional fixtures under `docs/`.
- `Dockerfile` — container build for hosted deployment.

### Commands

- Install: `pnpm install`.
- Type check: `pnpm run type`.
- Test: `pnpm test` (vitest, single file: `pnpm test path/to/file.test.ts`).
- Lint: `pnpm run lint` ; Format: `pnpm run format` ; Check all: `pnpm run check` ; Fix: `pnpm run fix`.
- Build: `pnpm run build` ; Run locally: `node index.js`.

### MCP-specific notes

- The MCP protocol is stdio-framed JSON-RPC; nothing the server prints to stdout outside the framing is recoverable by the client. Use `getDefaultLogger()` (which writes to stderr) for diagnostics, never `console.log`.
- Tool descriptions are part of the wire contract — changing one is a breaking change for clients that key on the description text. Bump the server version when modifying tool descriptions or input schemas.
- Container image must include a CA bundle (the base image's `ca-certificates`) — the Socket API uses TLS and the MCP server can't prompt for a workaround at runtime.

### Testing

- Tests are `*.test.ts` co-located with sources (no `test/` dir convention).
- 🚨 **Never** use `--` before test paths — that runs ALL tests.
- Mock the Socket API surface; never let tests hit production.
