Skip to content

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:

RULE_FULL="$(ls -td ~/.claude/plugins/cache/borda-ai-rig/foundry/*/rules/_full/claude-config.md 2>/dev/null | head -1)"; [ -z "$RULE_FULL" ] && RULE_FULL="plugins/cc_foundry/rules/_full/claude-config.md"  # timeout: 5000

Path Rules

  • No hardcoded absolute user paths — use .claude/, ~/, or git rev-parse --show-toplevel
  • Artifact dirs belong at project root, not inside .claude/ — see artifact-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: 120000 only 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 Edit calls 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, parallel Greps, a TaskUpdate beside 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 see CSID
  • Python scripts: _CSID = os.environ.get("CSID") or os.environ.get("CLAUDE_CODE_SESSION_ID") or "shared" — never os.getppid(); temp base via os.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, not A–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:challenger adversarial 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 over general-purpose once 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 measured Agent()-backed subagent transcripts only, no Skill() invocation. Treat Skill() overhead as unverified until measured separately; don't assume it shares Agent()'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 AskUserQuestion tool 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-approve answer
  • Never git add -A / git add . (stage by name); never --no-verify; never --no-gpg-sign unless the user asks
  • Subject type(scope): detail or type: detail ≤50 chars naming the highest-tier change in git diff HEAD + git diff --stat HEAD; never draft from session memory; omit redundant scope
  • Evidence-only body: every clause traces to a +/- line in git 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; regular git push asks via AskUserQuestion every time, skill workflows included
  • Detect the default branch dynamically, never hardcode main/master; prefer git revert over reset --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> ; add Co-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