Claude config
§Bash Timeouts per-operation-class table, §Directory Navigation Commands permission-matcher rationale + worktree notes, §TMPDIR Sentinel Scoping verified-token-facts / wrong-form examples / exemption list / migration-debt note, §Agent/Skill Spawn Discipline worked rationale + threshold derivation have worked detail in
_full/claude-config.md. Resolve + Read when that section's own trigger applies — not needed for routine work:
Path Rules¶
- No hardcoded absolute user paths — use
.claude/,~/, orgit rev-parse --show-toplevel - Artifact dirs belong at project root, not inside
.claude/— seeartifact-lifecycle.md
Bash Timeouts¶
Every Bash call must include explicit timeout — 3× expected P90 duration. Never rely on default 120 s cap; fail fast, let caller retry.
- Use 3× fastest plausible time — not worst case
- Unknown-duration command → estimate P90 conservatively, 3× that, minimum
timeout: 15000 - Timed-out fast op = signal to investigate; frozen session is not
timeout: 120000only for test suites or builds, never network calls
Per-operation-class lookup table (gh commands, git, installs, test suites, build steps): _full/claude-config.md.
Turn Batching — one response, every independent call¶
A response is one billed turn, and a turn re-reads the entire live context. Measured 2026-09-29 over 14 days: mean 221,527 tok of cache read per turn, 69.2% of all spend. Splitting work across turns that could have shared one is therefore the most expensive habit available, and it costs the same whether the extra turn does real work or none.
- Consecutive edits to the same file go in one response — several
Editcalls together, never one call per turn. Measured: 180 runs of 3+ single-edit turns on one file, 513 turns collapsible. Exception: an edit whose content depends on the previous edit's result. - Independent tool calls of any kind batch — parallel
Reads, parallelGreps, aTaskUpdatebeside the next real call. Only a genuine data dependency justifies a second turn. - Per-fix narration still applies (
quality-gates.md§Reporting Findings) — batching changes the turn count, never what gets stated.
Directory Navigation Commands¶
Never combine directory navigation with command in single Bash call — always use two separate Bash calls:
# ✓ correct — two calls; CWD persists between calls
cd /path/to/dir
uv run pytest tests/
# ✗ wrong — all three forms cause same failure
cd /path && uv run pytest tests/
cd /path
uv run pytest tests/
cd /path || uv run pytest tests/
Working directory persists between Bash calls — two sequential calls equivalent. Applies to every command, not just worktrees. Why (permission-matcher first-token mechanism) + worktree-specific notes: _full/claude-config.md.
TMPDIR Sentinel Scoping¶
Cross-bash-block state (Bash tool state doesn't persist across separate tool invocations in a skill) persists via ${TMPDIR:-/tmp}/<name> sentinel files. /tmp is machine-global, not project- or session-scoped — a bare name collides if two Claude Code sessions run the same skill concurrently.
Rule: every bash block touching sentinels derives the session token once (first line), then every sentinel name takes -${CSID} as its terminal suffix (after any dynamic part, after any extension):
# ✓ correct — session-scoped, safe under concurrent sessions
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
echo "$RUN_DIR" > "${TMPDIR:-/tmp}/oss-review-run-dir-${CSID}"
# ✗ wrong — bare name collides; ✗ wrong — CLAUDE_SESSION_ID doesn't exist, $$ is a new PID per Bash call
Wrong-form examples in full + verified token facts (env var name, $$/$PPID behaviour, subagent inheritance): _full/claude-config.md.
Sentinel READS use read, never $(cat ...) — command substitution makes prefix allow-rules fail-closed → "Contains expansion" permission prompt in every subagent:
# ✓ correct — no substitution, allow rules match, shape lane of allow-dispatch covers compounds
IFS= read -r RUN_DIR < "${TMPDIR:-/tmp}/oss-review-run-dir-${CSID}" 2>/dev/null || RUN_DIR=""
# ✗ wrong — $() triggers "Contains expansion" prompt regardless of allow list
RUN_DIR=$(cat "${TMPDIR:-/tmp}/oss-review-run-dir-${CSID}" 2>/dev/null || echo "")
read takes the first line only, exits non-zero on a file without trailing newline — safe here since sentinel writes use echo (single line + newline); always append || VAR=<default> for the missing-file case. Multi-line payloads (JSON blobs, report content) aren't sentinels — keep $(cat ...) there, expect the prompt.
export(not plain assignment) — python/node children invoked in the same block must seeCSID- Python scripts:
_CSID = os.environ.get("CSID") or os.environ.get("CLAUDE_CODE_SESSION_ID") or "shared"— neveros.getppid(); temp base viaos.environ.get("TMPDIR") or (tempfile.gettempdir() if os.name == "nt" else "/tmp")(= shell${TMPDIR:-/tmp}) - Applies to every new sentinel added to any skill, hook, or bin script
- Args-derived suffixes alone (e.g.
${CLEAN_ARGS}, a PR number) are not sufficient — two sessions in two different projects invoking the same skill with the same args still collide;-${CSID}is still required alongside them, not instead of them - Cleanup (
rm -f) at skill completion must target the exact session-scoped filename, never a glob that could match another session's sentinel of the same base name
Cross-OS notes, exemption list (mktemp templates, non-CC-runtime sentinels), and existing-debt/migration-plan pointer: _full/claude-config.md.
List Range Label Discipline¶
When editing file with lettered/numbered list range labels (e.g. **Close-scenario archetypes (A–G):**):
- After any edit adding/removing list items, update all range labels in file — not just edited section
- Non-contiguous letter ranges: use explicit form
A–C, F–G, notA–G, when items missing - Scan entire file after edits to catch stale range labels elsewhere
Agent/Skill Spawn Discipline¶
Spawning Agent() costs fixed overhead — measured ~120,851 tok (~73 tool-calls' worth) plus ~12.0 s per spawn, regardless of task size. Gate below applies only to work-displacement spawns — a spawn chosen purely to move tool-calls out of the caller's context. Not for a spawn chosen for role isolation: distinct system prompt, adversarial independence, different model tier, or worktree. Isolation value isn't priced by the ~73-call figure, so isolation-motivated spawns are exempt from the threshold, not exceptions to it.
- Work-displacement spawn (no isolation need — same role would do, just fewer tokens in caller context): estimate tool-calls first. Under ~73 → do it inline with native tools (Read/Grep/Glob/Edit/Bash), no spawn.
- Isolation-motivated spawn (distinct role/system-prompt, adversarial check, different model tier, or worktree required) → spawn regardless of call count; the isolation is the point, not a byproduct. Named standing examples:
bridge:review/foundry:challengeradversarial passes, any role-specialist dispatch mandated by another rule. - One subagent per independent subtask, not one per trivial step — batch related work-displacement spawns into a single spawn instead of a spawn-per-file or spawn-per-check. Cap the batch: only while the summed load of everything batched into it stays under the ~55-tool-call upper bound below — past that, split into parallel spawns (grouped under the bound) instead of one oversized batch. Worked stall evidence:
_full/claude-config.md. - A specialist exists for the domain (
foundry:sw-engineer,foundry:curator, etc.) → prefer it overgeneral-purposeonce a spawn is warranted under either bullet above. - Keep each spawned agent near ~55 tool-calls; past ~60 it risks stalling without returning an envelope, forcing reconstruction from disk — split oversized work across agents, not more small ones.
Skill()cost not covered by the measured figure above — regression measuredAgent()-backed subagent transcripts only, noSkill()invocation. TreatSkill()overhead as unverified until measured separately; don't assume it sharesAgent()'s constant, especially across model tiers.- Worked rationale + threshold derivation:
_full/claude-config.md.
Parallel Spawn Ceilings — by Model Tier¶
In-flight Agent() ceilings per model tier, summed across every step/phase due in one response: haiku 20 · sonnet 8 · opus 5 — separate pools per tier, grown in waves of ≤5; pool = the spawn's effective model (an Agent(..., model=…) override counts). Tier roster table + multi-phase capping: agent-spawn.md §Parallel Spawn Ceilings — injected on the first Agent() call, loaded with skill/agent files.
Lint/Format — Pinned Hooks, Not Bare Tools¶
Project pins lint/format tools via .pre-commit-config.yaml → never invoke them directly (ruff, eslint, mdformat, …; version/config drift vs CI); run pre-commit run <hook-id> --files <path>, ad-hoc checks included. Hook missing → add it to the config; no pin → direct tool use fine. Command forms: python-code.md §Lint/Format.
Instruction-File Compression Gate¶
Applies only to instruction files an LLM host loads (AGENTS.md, CLAUDE.md, rule, skill, or agent-definition files), never to ordinary docs or code. Compressing or structurally reformatting one = behavior-sensitive: byte-exact pre-change backup first, prove complete semantic preservation, restore the pre-change file on any loss; unresolved difference blocks completion. Five-step gate: markdown.md §Instruction-File Compression Gate (auto-loads on first access of any *.md) — Read before compressing.
Ask Before Acting on Unknown Cause¶
When user asks "why" about something (deleted content, unexpected state, missing items) and cause unknown:
- Never act — do not restore, revert, or modify anything
- State clearly cause unknown and why (e.g. "pre-session change not made by me")
- Call
AskUserQuestiontool directly — prose questions in brackets ([AskUserQuestion: ...],[Invoking AskUserQuestion: ...]) do NOT satisfy this requirement; only actual tool invocation does
Restoring without being asked = overstepping. "Why" = question, not request to fix.
Git Commit & Push — Hard Bans¶
Always on, format bullets included: the full commit rule (git-commit.md) is injected by rule-inject.js after the first commit-creating git call (add, commit, push, merge, …) of a session or subagent runs, beside that call's result — after the first message was drafted. Before drafting any commit message, Read ~/.claude/rules/foundry-git-commit.md and its _full/git-commit.md unless already in context.
- Never commit without authority: skill workflow step, explicit same-turn user request, local Git grant, or same-turn
git-approveanswer - Never
git add -A/git add .(stage by name); never--no-verify; never--no-gpg-signunless the user asks - Subject
type(scope): detailortype: detail≤50 chars naming the highest-tier change ingit diff HEAD+git diff --stat HEAD; never draft from session memory; omit redundant scope - Evidence-only body: every clause traces to a
+/-line ingit diff HEAD; no body line wrap; no GitHub auto-links (#N,@name); no non-VCS paths (/tmp/,~/.claude/) - Force-push (
-f/--force/--force-with-lease) forbidden on any branch, always; regulargit pushasks viaAskUserQuestionevery time, skill workflows included - Detect the default branch dynamically, never hardcode
main/master; prefergit revertoverreset --hard, merge commits over rebase - Co-author trailer on every commit, after a
---separator:Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>; addCo-authored-by: Codex <codex@openai.com>when Codex shaped the outcome; run the Codex-activity check from_full/git-commit.md§Co-authors before drafting — replaces the host's default attribution line