MANDATORY: Act as principal-level engineer. Follow these guidelines exactly.
Identify users by git credentials and use their actual name. Use "you/your" when speaking directly; use names when referencing contributions.
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.
Forbidden in the primary checkout:
git stash— shared store; another session canpopyoursgit add -A/git add .— sweeps files from other sessionsgit checkout <branch>/git switch <branch>— yanks the working tree out from under another sessiongit reset --hardagainst a non-HEAD ref — discards another session's commits
Required for branch work: spawn a worktree.
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
git worktree add -b <task-branch> ../<repo>-<task> "$BASE"
cd ../<repo>-<task>
# edit / commit / push from here; primary checkout is untouched
git worktree remove ../<repo>-<task>The BASE lookup resolves the remote's default branch — usually main, but legacy repos still use master. Never hard-code one; use git symbolic-ref refs/remotes/origin/HEAD (or fall back to main if the remote isn't set). See Default branch fallback below.
Required for staging: surgical git add <specific-file>. Never -A / ..
Never revert files you didn't touch. If git status shows unfamiliar changes, leave them — they belong to another session, an upstream pull, or a hook side-effect.
The umbrella rule: never run a git command that mutates state belonging to a path other than the file you just edited.
Always favor main and fall back to master when scripting git operations that target the default branch. Never hard-code either name — fleet repos are mostly on main, but a few legacy / vendored repos still use master, and a script that hard-codes main silently no-ops on those.
The canonical lookup, in order of preference:
# Best: ask the remote what its HEAD points to
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
# Fallback 1: prefer main if it exists
if [ -z "$BASE" ] && git show-ref --verify --quiet refs/remotes/origin/main; then
BASE=main
fi
# Fallback 2: fall back to master if main doesn't exist
if [ -z "$BASE" ] && git show-ref --verify --quiet refs/remotes/origin/master; then
BASE=master
fi
# Last resort: assume main and let the next git command fail loudly
BASE="${BASE:-main}"Apply this in: worktree creation, base-ref resolution for git diff / git rev-list, PR base detection in scripts, default-branch comparisons in skills, hook scripts that walk history. Documentation and CLAUDE.md examples can write main for clarity, but the underlying scripts must do the lookup.
The order main → master matches fleet reality (overwhelming majority on main); reversing it would silently pick the wrong branch in repos that have both (e.g., during a rename migration).
🚨 The four rules below have hooks that re-print the rule on every public-surface git / gh command. The rules apply even when the hooks are not installed.
- Real customer / company names — never write one into a commit, PR, issue, comment, or release note. Replace with
Acme Incor 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|dispatchorgh api …/dispatches. Dispatches are irrevocable. The user runs them manually. Bypass: agh workflow runwith-f dry-run=trueis allowed when the target workflow declares adry-run:input underworkflow_dispatch.inputsand no force-prod override (-f release=true/-f publish=true/-f prod=true) is set. - Workflow input naming —
workflow_dispatch.inputskeys are kebab-case (dry-run,build-mode), not snake_case. The release-workflow-guard hook only recognizes kebab; adry_runinput silently fails the dry-run bypass.
- 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=….
🚨 When the user asks for a version bump (bump to vX.Y.Z, tag X.Y.Z, release X, etc.), follow this sequence exactly. Order matters — skipping or reordering steps produces broken releases.
-
Pre-bump prep, in this order (each must finish clean before the next):
pnpm run updatepnpm ipnpm run fix --allpnpm run check --all
If any step surfaces failures, fix them before continuing. Don't bump a broken tree.
-
CHANGELOG entry — public-facing only. The new
## [X.Y.Z]block describes what a downstream consumer needs to know to upgrade. Include: new exports, removed exports, renamed exports, signature changes, behavioral changes, perf characteristics they will measure, migration recipes. Exclude internal refactors, file moves, test reorg, primordials cleanup, lint passes,chore(sync)cascades, build-script tweaks — these are noise to the consumer. Use Keep-a-Changelog sections (Added / Changed / Removed / Renamed / Fixed / Performance / Migration). Source the raw list withgit log <prev-tag>..HEAD --pretty="%s"and filter to consumer-visible commits only. -
The bump commit is the LAST commit on the release. If a session has other unrelated work to commit, those land first; the
chore: bump version to X.Y.Z(carrying bothpackage.jsonandCHANGELOG.md) is the tip of the branch when tagging. If a version-bump commit already exists earlier in history, rebase it forward so it ends up at the tip. -
Tag at the end:
git tag vX.Y.Zat the bump commit, then push the tag. -
Do NOT dispatch the publish workflow. Per the Public-surface hygiene rule, releases are user-triggered. Stop after the tag push; the user runs the publish workflow manually.
Why: Bisecting from main past the tag must not land on a temporarily-broken state. git describe is cleaner when the bump is the tip. The pre-bump prep wave catches dependency drift, formatting drift, and type drift that consumers would otherwise hit on first install. The public-facing-only filter is the difference between a changelog people read and a changelog people skip.
🚨 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.
- Package manager:
pnpm. Run scripts viapnpm run foo --flag, neverfoo:bar. Afterpackage.jsonedits,pnpm install. - 🚨 NEVER use
npx,pnpm dlx, oryarn dlx— usepnpm exec <package>orpnpm run <script># socket-hook: allow npx - Backward compatibility — FORBIDDEN to maintain. Actively remove when encountered.
- Full ruleset (packageManager field,
.config/placement,.mtsrunners, soak window, shallow submodules, monorepoengines.node) indocs/claude.md/tooling.md.
🚨 If you see a lint error, type error, test failure, broken comment, or stale comment anywhere in your reading window — fix it. Don't label it "pre-existing" and skip past. The label is a tell that you're rationalizing avoiding work; the user reads "pre-existing" the same as "I noticed but chose not to."
The only exceptions:
- The fix is genuinely out of scope (a 2000-line refactor would derail a one-line bug fix). State the trade-off explicitly and ask before deferring.
- You don't have permission (the file belongs to another session per the parallel-Claude rule).
In all other cases: fix it in the same commit, or in a sibling commit on the same branch. Never assume someone else will get to it.
🚨 An issue being unrelated to the task is not a reason to defer it — it's a reason to treat it as critical and fix it immediately. Unrelated bugs are exactly the bugs nobody is currently looking for; if you walk past one, no one else will catch it either. The instinct to "stay focused on the task" is how regressions accumulate.
When you spot an unrelated bug, broken comment, dead branch, type error, failing test, or stale config:
- Stop the current task.
- Fix the unrelated issue first, in its own commit on the same branch (or a sibling commit if scope demands it).
- Resume the original task.
If the fix is genuinely too large to bundle (a 2000-line refactor on a one-line bug), state the trade-off explicitly and ask before deferring — same exception as the "no pre-existing excuse" rule. Otherwise: unrelated = critical = fix now.
🚨 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. - 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) beforegit 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.
🚨 Reverting tracked changes or bypassing a hook (--no-verify, DISABLEPRECOMMIT*, --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/bypass-phrases.md.
🚨 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. Cross-fleet variants become a Drift watch task — open chore(sync): cascade <fix>.
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. Discipline: .claude/skills/_shared/compound-lessons.md.
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).
🚨 Drift across fleet repos is a defect, not a feature. When you see two socket-* repos pinning different versions of the same shared resource — a tool in external-tools.json, a workflow SHA, a CLAUDE.md fleet block, an action in .github/actions/, an upstream submodule SHA, a hook in .claude/hooks/ — opt for the latest. The repo with the newer version is the source of truth; older repos catch up.
Where drift commonly hides:
external-tools.json— pnpm/zizmor/sfw versions + per-platform sha256ssocket-registry/.github/actions/*— composite-action SHAs pinned in consumer workflowstemplate/CLAUDE.md<!-- BEGIN FLEET-CANONICAL -->block — must be byte-identical across the fleettemplate/.claude/hooks/*— same hook, same code- lockstep.json
pinned_sharows — upstream submodules tracked by socket-btm .gitmodules# name-versionannotations- pnpm/Node
packageManager/enginesfields
How to check:
- If you're editing one of these in repo A, grep the same thing in repos B/C/D. If A is older, bump A first; if A is newer, plan a sync to B/C/D.
socket-registry'ssetup-and-installaction is the canonical source for tool SHAs. Diverging from it is drift.socket-wheelhouse'stemplate/tree is the canonical source for.claude/, CLAUDE.md fleet block, and hook code. Diverging is drift.- Run
pnpm run sync-scaffolding(in repos that have it) to surface drift programmatically.
Never silently let drift sit. Either reconcile in the same PR or open a follow-up PR titled chore(sync): cascade <thing> from <newer-repo> and link it.
🚨 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/no-local-fork-canonical.md.
- Comments — default to none. When you do write one, audience is a junior dev: explain the constraint, the hidden invariant, the "why this and not the obvious thing." No teacher-tone.
- Completion — never leave
TODO/FIXME/XXX/ shims / stubs / placeholders. Finish 100%. nullvsundefined— useundefined.nullonly for__proto__: nullor external APIs.- HTTP — never
fetch(). UsehttpJson/httpText/httpRequestfrom@socketsecurity/lib/http-request. - File deletion —
safeDelete()/safeDeleteSync()from@socketsecurity/lib/fs. Neverfs.rm/fs.unlink/rm -rfdirectly. - Edits — Edit tool, never
sed/awk. - Full ruleset (object literals, imports, subprocesses, file existence, generated reports, sorting, Promise.race, Safe suffix,
node:smol-*modules, inclusive language) indocs/claude.md/code-style.md. See alsodocs/claude.md/sorting.mdanddocs/claude.md/inclusive-language.md.
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/file-size.md.
- 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.jsonexplicitly. 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 onpnpm run format. Canonical script bodies inmanifest.mtsalready encode the flag; the sync-scaffolding gate rewrites drifted scripts back to the canonical form.
Full rationale + cascade behavior in docs/claude.md/lint-rules.md.
A path is constructed exactly once. Everywhere else references the constructed value.
- Within a package: every script imports its own
scripts/paths.mts. Nopath.join('build', mode, …)outside that module. - Across packages: package B imports package A's
paths.mtsvia the workspaceexportsfield. Neverpath.join(PKG, '..', '<sibling>', 'build', …). - Workflows / Dockerfiles / shell can't
importTS — construct once, reference by output /ENV/ variable. - Canonical layout: build outputs live at
<package-root>/build/<mode>/<platform-arch>/out/Final/<artifact>, wheremode ∈ {dev, prod}andplatform-archis 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'sscripts/paths.mtsexportsPACKAGE_ROOT,BUILD_ROOT, andgetBuildPaths(mode, platformArch)returning at minimumoutputFinalDir+outputFinalFile/outputFinalBinary.
Three-level enforcement: .claude/hooks/path-guard/ blocks at edit time; 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.
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.
- 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.
- Default to perfectionist when you have latitude. "Works now" ≠ "right."
- 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.
An error message is UI. The reader should fix the problem from the message alone. Four ingredients in order:
- What — the rule, not the fallout (
must be lowercase, notinvalid). - Where — exact file / line / key / field / flag.
- Saw vs. wanted — the bad value and the allowed shape or set.
- 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. Full guidance in docs/claude.md/error-messages.md.
🚨 Never emit the raw value of any secret to tool output, commits, comments, or replies. The .claude/hooks/token-guard/ PreToolUse hook blocks the deterministic patterns; when it blocks, rewrite — don't bypass. Redact token / jwt / api_key / secret / password / authorization fields when citing API responses.
Socket API token env var — canonical fleet name is SOCKET_API_TOKEN (legacy SOCKET_API_KEY / SOCKET_SECURITY_API_TOKEN / SOCKET_SECURITY_API_KEY accepted as aliases for one cycle). Don't confuse 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/token-hygiene.md.
/scanning-security— AgentShield + zizmor audit/scanning-quality— quality analysis- Shared subskills in
.claude/skills/_shared/ - Handing off to another agent — see
docs/claude.md/agent-delegation.md. - Skill scope tiers (fleet / partial / unique), the
updatingumbrella +updating-*siblings convention, and thescripts/run-skill-fleet.mtscross-fleet runner indocs/claude.md/agents-and-skills.md.
- Build:
pnpm run build(smart) |--force|pnpm run build:cli|pnpm run build:sea - Test:
pnpm test(monorepo root) |pnpm --filter @socketsecurity/cli run test:unit <path> - Lint:
pnpm run lint| Type check:pnpm run type| Check all:pnpm run check - Fix:
pnpm run fix| Dev:pnpm dev(watch) | Run built:node packages/cli/dist/index.js <args>
- 🚨 NEVER use
--before test file paths — runs ALL tests - Always build before testing:
pnpm run build:cli - Update snapshots:
pnpm testu <path>or--updateflag - NEVER write source-code-scanning tests — verify behavior, not string patterns
- Simple (<200 LOC, no subcommands): single
cmd-*.mts - Complex:
cmd-*.mts+handle-*.mts+output-*.mts+fetch-*.mts
Advice and critical assessment ONLY — never for making code changes. Consult before complex optimizations (>30min).