Skip to content

Latest commit

 

History

History
427 lines (288 loc) · 30.5 KB

File metadata and controls

427 lines (288 loc) · 30.5 KB

CLAUDE.md

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

📚 Fleet Standards

Identifying users

Identify users by git credentials and use their actual name. Use "you/your" when speaking directly; use names when referencing contributions.

Parallel Claude sessions

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 can pop yours
  • git add -A / git add . — sweeps files from other sessions
  • git checkout <branch> / git switch <branch> — yanks the working tree out from under another session
  • git reset --hard against 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.

Public-surface hygiene

🚨 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 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 or gh api …/dispatches. Dispatches are irrevocable. The user runs them manually.

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=….

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/programmatic-claude-lockdown/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
  • Soak window (pnpm-workspace.yaml minimumReleaseAge, default 7 days) — never add packages to minimumReleaseAgeExclude in CI. Locally, ASK before adding (security control).
  • Backward compatibility — FORBIDDEN to maintain. Actively remove when encountered.

Code style

  • 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.
  • null vs undefined — use undefined. null is allowed only for __proto__: null or external API requirements.
  • Object literals{ __proto__: null, ... } for config / return / internal-state.
  • Imports — no dynamic await import(). node:fs cherry-picks (existsSync, promises as fs); path / os / url / crypto use default imports. Exception: fileURLToPath from node:url.
  • HTTP — never fetch(). Use httpJson / httpText / httpRequest from @socketsecurity/lib/http-request.
  • File existenceexistsSync from node:fs. Never fs.access / fs.stat-for-existence / async fileExists wrapper.
  • File deletion — route every delete through safeDelete() / safeDeleteSync() from @socketsecurity/lib/fs. Never fs.rm / fs.unlink / fs.rmdir / rm -rf directly — even for one known file.
  • Edits — Edit tool, never sed / awk.
  • Inclusive language — see docs/references/inclusive-language.md for the substitution table.
  • Sorting — sort lists alphanumerically; details in docs/references/sorting.md. When in doubt, sort.
  • Promise.race / Promise.any in loops — never re-race a pool that survives across iterations (the handlers stack). See .claude/skills/promise-race-pitfall/SKILL.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.
  • Across packages: package B imports package A's paths.mts via the workspace exports field. Never path.join(PKG, '..', '<sibling>', 'build', …).
  • Workflows / Dockerfiles / shell can't import TS — 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.

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.

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.
  • 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.

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. Full guidance in docs/references/error-messages.md.

Token hygiene

🚨 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.

Agents & skills

  • /security-scan — AgentShield + zizmor audit
  • /quality-scan — quality analysis
  • Shared subskills in .claude/skills/_shared/

USER CONTEXT

  • Identify users by git credentials; use their actual name, never "the user"
  • Use "you/your" when speaking directly; use names when referencing contributions

PARALLEL CLAUDE SESSIONS - WORKTREE REQUIRED

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 can pop yours.
    • 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 --hard against 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 status shows files you didn't modify, those belong to another session, an upstream pull, or a hook side-effect — leave them alone. Specifically: do not run git checkout -- <unrelated-path> to "clean up" the diff before committing, and do not include unrelated paths in git 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.

PRE-ACTION PROTOCOL

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

VERIFICATION PROTOCOL

  1. Run the actual command — execute, don't assume
  2. State what you verified, not just "looks good"
  3. FORBIDDEN: Claiming "Done" when tests show failures
  4. Run type-check/lint if configured; fix ALL errors before reporting done
  5. Re-read every modified file; confirm nothing references removed items

CONTEXT & EDIT SAFETY

  • 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

JUDGMENT PROTOCOL

  • 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."

SCOPE PROTOCOL

  • Do not add features or improvements beyond what was asked
  • Simplest approach first; flag architectural flaws and wait for approval

COMPLETION PROTOCOL

  • 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

SELF-EVALUATION

  • 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

HOUSEKEEPING

  • Offer to checkpoint before risky changes
  • Flag files >400 LOC for potential splitting

ERROR MESSAGES

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:

  1. What — the rule that was broken (e.g. "must be lowercase"), not the fallout ("invalid").
  2. Where — the exact file, line, key, field, or CLI flag. Not "somewhere in config".
  3. Saw vs. wanted — the bad value and the allowed shape or set.
  4. Fix — one concrete action, in imperative voice (rename the key to …, not the 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 underscore covers 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 / joinOr from @socketsecurity/lib/arraysmust 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) — replaces e instanceof Error. Cross-realm-safe.
  • isErrnoException(e) — replaces 'code' in err guards. Narrows to NodeJS.ErrnoException.
  • errorMessage(e) — replaces e instanceof Error ? e.message : String(e) and any 'Unknown error' fallback. Walks the cause chain.
  • errorStack(e) — cause-aware stack or undefined.

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.

ABSOLUTE RULES

  • 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, or yarn dlx — use pnpm exec or pnpm run

  • minimumReleaseAge: NEVER add packages to minimumReleaseAgeExclude in 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, or gh 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 a dry_run input 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 claude CLI or @anthropic-ai/claude-agent-sdk) MUST set all four lockdown flags: --tools/tools, --allowedTools/allowedTools, --disallowedTools/disallowedTools, and --permission-mode dontAsk/permissionMode: 'dontAsk'. NEVER default mode in headless contexts (falls through to a missing canUseTool → undefined behavior). NEVER bypassPermissions. See .claude/skills/programmatic-claude-lockdown/SKILL.md for the recipe + reference impl (socket-lib/tools/prim/src/disambiguate.mts).

📚 SHARED STANDARDS

  • 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: foo after you've added security-fix and docs commits to it is now a lie. Use gh 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 --flag over foo:bar variants
  • Dependencies: After package.json edits, run pnpm 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) or safeDeleteSync() (sync) from @socketsecurity/lib/fs. NEVER reach for fs.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() — use httpJson/httpText/httpRequest from @socketsecurity/lib/http-request
  • File existence: ALWAYS existsSync from node:fs. NEVER fs.access, fs.stat-for-existence, or an async fileExists wrapper. Import form: import { existsSync, promises as fs } from 'node:fs'.

1 path, 1 reference

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 (or lib/paths.mts). No path.join('build', mode, ...) outside that module.
  • 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', ...).
  • 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 / ENV, reference by name thereafter. Each canonical construction carries a comment naming the source-of-truth paths.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/ blocks Edit/Write calls that would introduce a violation in a .mts/.cts file at edit time.
  • Gatescripts/check-paths.mts runs in pnpm check. Fails the build on any violation that isn't allowlisted in .github/paths-allowlist.yml.
  • 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.

The mantra is intentionally short so it sticks: 1 path, 1 reference. When in doubt, find the canonical owner and import from it.

Inclusive Language

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.

Sorting

Sort lists alphanumerically (literal byte order, ASCII before letters). Apply this to:

  • Config listspermissions.allow / permissions.deny in .claude/settings.json, external-tools.json checksum keys, allowlists in workflow YAML.
  • Object key entries — sort keys in plain JSON config + return-shape literals + internal-state objects. (Exception: __proto__: null always 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 say import type follow 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 export keyword 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.
  • Set constructor argumentsnew Set([...]) and new 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.

EMOJI & OUTPUT STYLE

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.


🏗️ PURL-SPECIFIC

Architecture

TypeScript implementation of Package URL spec (ECMA-427), compiled to CommonJS.

  • src/package-url.ts — main exports and API
  • src/purl-types/ — type-specific handlers (npm, pypi, maven, etc.)
  • src/error.js — PurlError
  • dist/ — CommonJS build output

Commands

  • Build: pnpm build (pnpm build --watch for 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

Agents & Skills

  • /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/

Error Handling — PurlError Patterns

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).

TypeScript Patterns

  • 🚨 Type imports MUST be separate import type statements, never inline type in value imports
  • With exactOptionalPropertyTypes: assign conditionally, never prop = value ?? undefined
  • 🚨 Use bracket notation with index signatures: obj['prop']?.['method']
  • NEVER use process.chdir() — pass { cwd } options instead

Testing

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.

CI

🚨 MANDATORY: Pin CI workflow refs to full SHA — @<full-sha> # main

TOKEN HYGIENE — NON-NEGOTIABLE

🚨 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):

  1. Never run env, printenv, export -p, or set (no args) — they print everything.
  2. Never cat/head/tail/less/more a .env* file without piping through a redactor (sed 's/=.*/=<redacted>/' or similar). If you only need the keys, use grep -v '^#' .env.local | cut -d= -f1.
  3. Never run a curl that carries -H "Authorization: ..." with output going to unfiltered stdout. Either redirect to /dev/null, save to a file, or pipe to jq/grep/head.
  4. 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, authorization field to <redacted> before including in your reply.
  • When showing .env.local or 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 over export FOO=bar && ... chains, so the value never appears in the Bash tool's command string.