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.
git worktree add -b <task-branch> ../<repo>-<task> main
cd ../<repo>-<task>
# edit / commit / push from here; primary checkout is untouched
git worktree remove ../<repo>-<task>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.
🚨 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.
- 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=….
🚨 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/programmatic-claude-lockdown/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 - Soak window (pnpm-workspace.yaml
minimumReleaseAge, default 7 days) — never add packages tominimumReleaseAgeExcludein CI. Locally, ASK before adding (security control). - Backward compatibility — FORBIDDEN to maintain. Actively remove when encountered.
- Comments — default to none. Write one only when the WHY is non-obvious to a senior engineer.
- Completion — never leave
TODO/FIXME/XXX/ shims / stubs / placeholders. Finish 100%. If too large for one pass, ask before cutting scope. nullvsundefined— useundefined.nullis allowed only for__proto__: nullor external API requirements.- Object literals —
{ __proto__: null, ... }for config / return / internal-state. - Imports — no dynamic
await import().node:fscherry-picks (existsSync,promises as fs);path/os/url/cryptouse default imports. Exception:fileURLToPathfromnode:url. - HTTP — never
fetch(). UsehttpJson/httpText/httpRequestfrom@socketsecurity/lib/http-request. - File existence —
existsSyncfromnode:fs. Neverfs.access/fs.stat-for-existence / asyncfileExistswrapper. - File deletion — route every delete through
safeDelete()/safeDeleteSync()from@socketsecurity/lib/fs. Neverfs.rm/fs.unlink/fs.rmdir/rm -rfdirectly — even for one known file. - Edits — Edit tool, never
sed/awk. - Inclusive language — see
docs/references/inclusive-language.mdfor the substitution table. - Sorting — sort lists alphanumerically; details in
docs/references/sorting.md. When in doubt, sort. Promise.race/Promise.anyin loops — never re-race a pool that survives across iterations (the handlers stack). See.claude/skills/promise-race-pitfall/SKILL.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.
Three-level enforcement: .claude/hooks/path-guard/ blocks at edit time; scripts/check-paths.mts is the whole-repo gate run by pnpm check; /path-guard 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.
- 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/references/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 (literal token shapes, env dumps, .env* reads, unfiltered curl -H "Authorization:", sensitive-name commands without redaction). When the hook blocks a command, rewrite — don't bypass.
Behavior the hook can't catch: redact token / jwt / access_token / refresh_token / api_key / secret / password / authorization fields when citing API responses. Show key names only when displaying .env.local. If a user pastes a secret, treat it as compromised and ask them to rotate.
Full hook spec in .claude/hooks/token-guard/README.md.
/security-scan— AgentShield + zizmor audit/quality-scan— quality analysis- Shared subskills in
.claude/skills/_shared/
- Identify users by git credentials; use their actual name, never "the user"
- 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 and silently destroy or hijack the other session's work.
-
FORBIDDEN in the primary checkout (the one another Claude may be editing):
git stash— shared stash store; another session canpopyours.git add -A/git add .— sweeps files belonging to other sessions.git checkout <branch>/git switch <branch>— yanks the working tree out from under another session.git reset --hardagainst a non-HEAD ref — discards another session's commits.
-
REQUIRED for branch work: spawn a worktree instead of switching branches in place. Each worktree has its own HEAD, so branch operations inside it are safe.
# From the primary checkout — does NOT touch the working tree here. git worktree add -b <task-branch> ../<repo>-<task> main cd ../<repo>-<task> # edit, commit, push from here; the primary checkout is untouched. cd - git worktree remove ../<repo>-<task>
-
REQUIRED for staging: surgical
git add <specific-file> [<file>…]with explicit paths. Never-A/.. -
If you need a quick WIP save: commit on a new branch from inside a worktree, not a stash.
-
NEVER revert files you didn't touch. If
git statusshows files you didn't modify, those belong to another session, an upstream pull, or a hook side-effect — leave them alone. Specifically: do not rungit checkout -- <unrelated-path>to "clean up" the diff before committing, and do not include unrelated paths ingit add. Stage only the explicit files you edited.
The umbrella rule: never run a git command that mutates state belonging to a path other than the file you just edited.
MANDATORY: Review CLAUDE.md before any action. No exceptions.
- Before ANY structural refactor on a file >300 LOC: remove dead code first, commit separately
- Multi-file changes: phases of ≤5 files, verify each before the next
- Study existing code before building
- Work from raw error data, not theories
- On "yes", "do it", or "go": execute immediately, no plan recap
- Run the actual command — execute, don't assume
- State what you verified, not just "looks good"
- FORBIDDEN: Claiming "Done" when tests show failures
- Run type-check/lint if configured; fix ALL errors before reporting done
- Re-read every modified file; confirm nothing references removed items
- After 10+ messages: re-read files before editing
- Read files >500 LOC in chunks
- Before every edit: re-read. After every edit: re-read to confirm
- When renaming: search direct calls, type refs, string literals, dynamic imports, re-exports, tests
- Tool results over 50K chars are silently truncated — narrow scope and re-run if incomplete
- For tasks touching >5 files: use sub-agents with worktree isolation
- If the user's request is based on a misconception, say so before executing
- If you spot a bug adjacent to what was asked, flag it: "I also noticed X — want me to fix it?"
- You are a collaborator, not just an executor
- Fix warnings when you find them (lint, type-check, build, runtime) — don't leave them for later
- Default to perfectionist mindset: when you have latitude to choose, pick the maximally correct option — no shortcuts, no cosmetic deferrals. Fix state that looks stale even if not load-bearing. If pragmatism is the right call, the user will ask for it explicitly. "Works now" ≠ "right."
- Do not add features or improvements beyond what was asked
- Simplest approach first; flag architectural flaws and wait for approval
- Finish 100% before reporting — never claim done at 80%
- Fix forward, don't revert (reverting requires explicit user approval)
- After EVERY code change: build, test, verify, commit as one atomic unit
- Present two views before calling done: what a perfectionist would reject vs. what a pragmatist would ship — and let the user decide. If the user gives no signal, default to perfectionist: do the fuller fix.
- If a fix fails twice: stop, re-read top-down, state where the mental model was wrong
- Offer to checkpoint before risky changes
- Flag files >400 LOC for potential splitting
An error message is UI. The reader should be able to fix the problem from the message alone, without opening your source.
Every message needs four ingredients, in order:
- What — the rule that was broken (e.g. "must be lowercase"), not the fallout ("invalid").
- Where — the exact file, line, key, field, or CLI flag. Not "somewhere in config".
- Saw vs. wanted — the bad value and the allowed shape or set.
- Fix — one concrete action, in imperative voice (
rename the key to …, notthe key was not renamed).
Length depends on the audience:
- Library API errors (thrown from a published package): terse. Callers may match on the message text, so every word counts. All four ingredients often fit in one sentence — e.g.
name "__proto__" cannot start with an underscorecovers rule, where (name), saw (__proto__), and implies the fix. - Validator / config / build-tool errors (developer reading a terminal): verbose. Give each ingredient its own words so the reader can find the bad record without re-running the tool.
- Programmatic errors (internal assertions, invariant checks): terse, rule only. No end user will see it; short keeps the check readable.
Rules for every message:
- Imperative voice for the fix —
add "filename" to part 3, not"filename" was missing. - Never "invalid" on its own.
invalid filename 'My Part'is fallout;filename 'My Part' must be [a-z]+ (lowercase, no spaces)is a rule. - On a collision, name both sides, not just the second one found.
- Suggest, don't auto-correct. Silently fixing state hides the bug next time.
- Bloat check: if removing a word keeps the information, drop it.
- For allowed-set / conflict lists, use
joinAnd/joinOrfrom@socketsecurity/lib/arrays—must be one of: ${joinOr(allowed)}reads better than a hand-formatted list.
Caught-value helpers from @socketsecurity/lib/errors (prefer these in scripts over hand-rolled checks; src/ uses primordial-guarded helpers from src/error.ts instead):
isError(e)— replacese instanceof Error. Cross-realm-safe.isErrnoException(e)— replaces'code' in errguards. Narrows toNodeJS.ErrnoException.errorMessage(e)— replacese instanceof Error ? e.message : String(e)and any'Unknown error'fallback. Walks thecausechain.errorStack(e)— cause-aware stack orundefined.
Examples:
- ✗
Error: invalid config→ ✓config.json: part 3 is missing "filename". Add a lowercase filename (e.g. "parsing"). - ✗
Error: invalid component→ ✓npm "name" component is required
See docs/references/error-messages.md for worked examples and anti-patterns.
-
Never create files unless necessary; always prefer editing existing files
-
Forbidden to create docs unless requested
-
🚨 NEVER leave
TODO,FIXME,XXX, shims, stubs, or placeholder code — finish 100%. If the task is too large for a single pass, inform the user and ask before cutting scope; don't silently reduce scope, and don't land half-work with a promise to fix it later. -
🚨 NEVER use
npx,pnpm dlx, oryarn dlx— usepnpm execorpnpm run -
minimumReleaseAge: NEVER add packages to
minimumReleaseAgeExcludein CI. Locally, ASK before adding — the age threshold is a security control. -
🚨 NEVER mention private repos or internal project names in commits, PR titles/descriptions/comments, issues, release notes, or any public-surface text. Internal codenames, unreleased product names, internal tooling repo names not on the public org page, customer names, partner names — none belong in public surfaces. Omit the reference entirely. Don't substitute a placeholder ("an internal tool", "a downstream consumer", etc.) — the placeholder itself is a tell that something is being elided. Rewrite the sentence to not need the reference at all.
-
🚨 NEVER trigger Publish / Release / Provenance / Build-Release workflows — no
gh workflow run,gh workflow dispatch, orgh api .../dispatches. Workflow dispatches are irrevocable: Publish workflows push npm versions (unpublishable after 24h), Build/Release workflows pin GitHub releases by SHA, container workflows push immutable tags. Even build workflows with adry_runinput still treat the dispatch itself as the prod trigger. The user runs workflow_dispatch jobs manually after CI passes on the release commit + tag — Claude never dispatches them. If the user asks for a publish, tell them to run the command in their own terminal (or the GitHub Actions UI). -
🚨 Programmatic Claude calls (workflows, skills, scripts that invoke
claudeCLI or@anthropic-ai/claude-agent-sdk) MUST set all four lockdown flags:--tools/tools,--allowedTools/allowedTools,--disallowedTools/disallowedTools, and--permission-mode dontAsk/permissionMode: 'dontAsk'. NEVERdefaultmode in headless contexts (falls through to a missingcanUseTool→ undefined behavior). NEVERbypassPermissions. See.claude/skills/programmatic-claude-lockdown/SKILL.mdfor the recipe + reference impl (socket-lib/tools/prim/src/disambiguate.mts).
- Commits: Conventional Commits
<type>(<scope>): <description>— NO AI attribution - Open PRs: when adding commits to an OPEN PR, ALWAYS update the PR title and description to match the new scope. A title like
chore: fooafter you've added security-fix and docs commits to it is now a lie. Usegh pr edit <num> --title "..." --body "..."(or--body-file) and rewrite the body so it reflects every commit on the branch, grouped by theme. The reviewer should be able to read the PR description and know what's in it without scrolling commits. - Scripts: Prefer
pnpm run foo --flagoverfoo:barvariants - Dependencies: After
package.jsonedits, runpnpm install - Backward Compatibility: 🚨 FORBIDDEN to maintain — actively remove when encountered
- Work Safeguards: MANDATORY commit + backup branch before bulk changes
- Safe Deletion: Route every filesystem delete through
safeDelete()(async) orsafeDeleteSync()(sync) from@socketsecurity/lib/fs. NEVER reach forfs.rm/fs.rmSync/fs.unlink/fs.unlinkSync/fs.rmdir/fs.rmdirSync/rm -rf— even for a single known file. The rule is "all deletes go through the safe helpers," not "except when the blast radius is small"; uniform routing is what keeps audit + retry + signal-abort behavior consistent. - HTTP Requests: NEVER use
fetch()— usehttpJson/httpText/httpRequestfrom@socketsecurity/lib/http-request - File existence: ALWAYS
existsSyncfromnode:fs. NEVERfs.access,fs.stat-for-existence, or an asyncfileExistswrapper. Import form:import { existsSync, promises as fs } from 'node:fs'.
A path is constructed exactly once. Everywhere else references the constructed value.
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.
- Within a package: every script imports its own
scripts/paths.mts(orlib/paths.mts). Nopath.join('build', mode, ...)outside that module. - Across packages: when package B consumes package A's output, B imports A's
paths.mtsvia the workspaceexportsfield. Neverpath.join(PKG, '..', '<sibling>', 'build', ...). - Workflows, Dockerfiles, shell scripts: they can't
importTS, so they construct the string once and reference it everywhere downstream. Workflows: a "Compute paths" step exposessteps.paths.outputs.final_dir; later steps read${{ steps.paths.outputs.final_dir }}. Dockerfiles/shell: assign once to a variable /ENV, reference by name thereafter. Each canonical construction carries a comment naming the source-of-truthpaths.mts. Re-building the same path in a second step is the violation, not referring to the constructed value many times. - Comments: may describe path structure with placeholders ("
<mode>/<arch>") but should not encode a complete literal path string. The import statement IS the comment.
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.
Three-level enforcement:
- Hook —
.claude/hooks/path-guard/blocksEdit/Writecalls that would introduce a violation in a.mts/.ctsfile at edit time. - Gate —
scripts/check-paths.mtsruns inpnpm check. Fails the build on any violation that isn't allowlisted in.github/paths-allowlist.yml. - Skill —
/path-guardaudits the repo and fixes findings;/path-guard checkreports only;/path-guard installdrops the gate + hook + rule into a fresh repo.
The mantra is intentionally short so it sticks: 1 path, 1 reference. When in doubt, find the canonical owner and import from it.
Use precise, neutral terms over historical metaphors that imply hierarchy or exclusion. The substitutes are not euphemisms — they're more accurate (a list of allowed values genuinely is an "allowlist"; "whitelist" is a metaphor that hides what the list does).
| Replace | With |
|---|---|
whitelist / whitelisted |
allowlist / allowed / allowlisted |
blacklist / blacklisted |
denylist / denied / blocklisted / blocked |
master (branch, process, copy) |
main (branch); primary / controller (process) |
slave |
replica, worker, secondary, follower |
grandfathered |
legacy, pre-existing, exempted |
sanity check |
quick check, confidence check, smoke test |
dummy (placeholder) |
placeholder, stub |
Apply across code (identifiers, comments, string literals), docs (READMEs, CLAUDE.md, markdown), config files (YAML, JSON), commit messages, PR titles/descriptions, and CI logs you control.
Two exceptions where the legacy term must remain (because changing it breaks something external):
- Third-party APIs / upstream code: when interfacing with an external API field literally named
whitelist, keep the field name; rename your local variable. E.g.const allowedDomains = response.whitelist. - Vendored upstream sources: don't rewrite vendored code (
vendor/**,upstream/**,**/fixtures/**). Patch around it if needed.
When you encounter a legacy term during unrelated work, fix it inline — don't defer.
Sort lists alphanumerically (literal byte order, ASCII before letters). Apply this to:
- Config lists —
permissions.allow/permissions.denyin.claude/settings.json,external-tools.jsonchecksum keys, allowlists in workflow YAML. - Object key entries — sort keys in plain JSON config + return-shape literals + internal-state objects. (Exception:
__proto__: nullalways comes first, ahead of any data keys.) - Import specifiers — sort named imports inside a single statement:
import { encrypt, randomDataKey, wrapKey } from './crypto.mts'. Imports that sayimport typefollow the same rule. Statement order is the project's existing convention (node:→ external → local → types) — that's separate from specifier order within a statement. - Method / function source placement — within a module, sort top-level functions alphabetically. Convention: private functions (lowercase / un-exported) sort first, exported functions second. The first-line
exportkeyword is the divider. - Array literals — when the array is a config list, allowlist, or set-like collection. Position-bearing arrays (e.g. argv, anything where index matters semantically) keep their meaningful order.
Setconstructor arguments —new Set([...])andnew SafeSet([...])literals. The runtime is order-insensitive, so source order is alphanumeric. Same rationale as Array literals: predictable diffs, no merge conflicts on insertions.
When in doubt, sort. The cost of a sorted list that didn't need to be is approximately zero; the cost of an unsorted list that did need to be is a merge conflict.
Terminal symbols (from @socketsecurity/lib/logger LOG_SYMBOLS): ✓ (green), ✗ (red), ⚠ (yellow), ℹ (blue), → (cyan). Color the icon only. Use yoctocolors-cjs (not ESM yoctocolors). Avoid emoji overload.
TypeScript implementation of Package URL spec (ECMA-427), compiled to CommonJS.
src/package-url.ts— main exports and APIsrc/purl-types/— type-specific handlers (npm, pypi, maven, etc.)src/error.js— PurlErrordist/— CommonJS build output
- Build:
pnpm build(pnpm build --watchfor dev) - Test:
pnpm test(specific file:pnpm test:unit path/to/file.test.mts) - Type check:
pnpm type - Lint:
pnpm lint - Check all:
pnpm check - Fix:
pnpm fix - Coverage:
pnpm cover(must maintain 100%) - Update snapshots:
pnpm testu
/security-scan— AgentShield + zizmor security audit/quality-scan— code quality analysis with specialized agents/quality-loop— scan and fix iteratively- Agents:
code-reviewer,security-reviewer,refactor-cleaner(in.claude/agents/) - Shared subskills in
.claude/skills/_shared/
PurlError (parser errors): no period, lowercase start (unless proper noun)
- Pattern:
{type} "{component}" component {violation} - Required:
"{component}" is a required component - Qualifier:
qualifier "{key}" {violation}
Error (argument validation): period, sentence case
- Example:
throw new Error('JSON string argument is required.')
Rules: Never throw on valid purls. Include { cause: e } when wrapping. No process.exit() in library code (OK in scripts/). Use catch (e) not catch (error).
- 🚨 Type imports MUST be separate
import typestatements, never inlinetypein value imports - With
exactOptionalPropertyTypes: assign conditionally, neverprop = value ?? undefined - 🚨 Use bracket notation with index signatures:
obj['prop']?.['method'] - NEVER use
process.chdir()— pass{ cwd }options instead
Vitest configs: .config/vitest.config.mts (threads, shared) and .config/vitest.config.isolated.mts (forks, full isolation).
File naming: *.test.mts for standard tests; *.isolated.test.mts for tests that mock globals or use vi.doMock().
- 🚨 NEVER use
--before test paths — runs ALL tests - Test style: functional tests over source scanning. Never read source files and assert on contents.
🚨 MANDATORY: Pin CI workflow refs to full SHA — @<full-sha> # main
🚨 Never emit the raw value of any secret to any tool output, commit message, comment, or assistant response. A Bash PreToolUse hook at .claude/hooks/token-hygiene/ enforces this programmatically — env/printenv/cat .env*/curl -H Authorization patterns without a redaction pipeline are refused before the tool runs.
Enforcement rules the hook encodes (applies to Bash tool calls):
- Never run
env,printenv,export -p, orset(no args) — they print everything. - Never
cat/head/tail/less/morea.env*file without piping through a redactor (sed 's/=.*/=<redacted>/'or similar). If you only need the keys, usegrep -v '^#' .env.local | cut -d= -f1. - Never run a
curlthat carries-H "Authorization: ..."with output going to unfiltered stdout. Either redirect to/dev/null, save to a file, or pipe tojq/grep/head. - Never construct a command that references a sensitive env var name (
*TOKEN*,*SECRET*,*PASSWORD*,*API_KEY*,*SIGNING_KEY*,*PRIVATE_KEY*,*AUTH*,*CREDENTIAL*) and writes to stdout without a redaction step — unless the command is a legitimate git/pnpm/npm/node/tsc operation that only surfaces names.
If the hook blocks you, the stderr output explains why and suggests a fix. Rewrite the command; don't bypass the hook.
Behavioral rules (things the hook can't catch):
- When citing an API response, redact any
token,jwt,access_token,refresh_token,api_key,secret,password,authorizationfield to<redacted>before including in your reply. - When showing
.env.localor similar, show key names only — never values. - If a user pastes a secret to you, treat the session's copy as compromised and ask them to rotate it. Never re-echo it.
- Prefer reading env values into subprocesses via
{ env: { ... } }spawn options overexport FOO=bar && ...chains, so the value never appears in the Bash tool's command string.