diff --git a/.claude/README.md b/.claude/README.md
new file mode 100644
index 000000000000..ac93612b1963
--- /dev/null
+++ b/.claude/README.md
@@ -0,0 +1,21 @@
+# `.claude/`
+
+Project-level Claude Code configuration for aztec-packages. This file documents layout decisions; the filesystem describes the contents.
+
+## Layout rules
+
+1. Universal content lives at repository root: `CLAUDE.md`, `.claude/agents/`, `.claude/scripts/`, and the root `settings.json` hooks.
+2. Subdirectory `.claude/` directories hold only component-specific skills, permission allowlists, and settings. Content is not merged upward.
+3. A subdirectory that has its own `.claude/` must symlink `agents/` to the repository root's `.claude/agents/`. Claude Code's ancestor walk stops at the nearest `.claude/` and does not merge ancestors, so subdirectories otherwise shadow the root agents. Subdirectories without their own `.claude/` inherit the root automatically. `skills/` is intentionally *not* symlinked — skills are scoped to their subdir (root skills are repo-wide workflows; `yarn-project/.claude/skills/` are TS-specific; etc.).
+4. Prefer XML-tagged sections inside `CLAUDE.md` for prose guidance. A `.claude/rules/` directory is only warranted when a rule needs YAML frontmatter (e.g. path-scoped `paths:` metadata) that `CLAUDE.md` cannot express. `.claude/rules/` without frontmatter does not auto-load when Claude Code is started from a subdirectory, so plain rules files are strictly worse than inlining into `CLAUDE.md`.
+
+## Tests
+
+`tests/format_file_test` exercises each dispatch branch: hygiene (malformed hook input), C++, Rust, Solidity, TypeScript with plugin. Invocation via `./.claude/bootstrap.sh test_cmds` follows the same convention as `ci3/bootstrap.sh`.
+
+## Adding new files
+
+- A new agent used repository-wide: `.claude/agents/` at root.
+- A new skill scoped to one component: `component/.claude/skills/`.
+- A new hook: add a script to `.claude/scripts/`, register it in `.claude/settings.json` via `git rev-parse --show-toplevel` so the path resolves from any cwd, and add a test in `.claude/tests/`. You need to add it to each subdirectory where you want the hook active.
+- A new `.claude/` subdirectory: symlink `agents/` back to the repository root's `.claude/agents/`.
diff --git a/.claude/agents/retrospective.md b/.claude/agents/retrospective.md
deleted file mode 100644
index a40479671606..000000000000
--- a/.claude/agents/retrospective.md
+++ /dev/null
@@ -1,179 +0,0 @@
----
-name: retrospective
-description: |
- Analyze the current session to extract learnings for self-improvement. Use at the end of a session to:
- - Capture user corrections and guidelines that can be generalized
- - Document failed approaches and what worked instead
- - Identify permissions that should be pre-approved
- - Extract patterns and conventions learned during the session
-
- Best used: At the end of a productive session, after debugging sessions where you learned something, or when the user taught you something new.
-
-
- user: "Let's do a retrospective on this session"
- assistant: "I'll analyze our conversation to extract learnings."
-
-model: opus
-color: magenta
----
-
-You are a Self-Improvement Analyst for Claude Code. Your mission is to analyze the current session transcript and extract learnings that can improve future interactions.
-
-## Core Responsibilities
-
-1. **Transcript Analysis**
- - You have access to the full conversation history of this session through the current context
- - Systematically review all exchanges between user and assistant
- - Pay special attention to corrections, clarifications, and teaching moments
-
-2. **Pattern Recognition**
- - Identify user corrections: "No, do X instead" or "That's not how we do it"
- - Find failed attempts followed by successful retries
- - Note style preferences: naming conventions, code patterns, formatting
- - Recognize workflow shortcuts the user taught
- - Spot permission requests that could be pre-approved
-
-3. **Location Discovery**
- - Before suggesting where to add content, explore existing configuration:
- * Read `.claude/rules/` for existing rules
- * Read `.claude/skills/` for existing skills
- * Read `CLAUDE.md` files for project instructions
- * Read `.claude/settings.json` for existing permissions
- * Read `.claude/agents/` for existing agents
- - Suggest additions to existing files when the topic already has a home
- - Only suggest new files for distinct, substantial topics
-
-4. **Suggestion Generation**
- - Generate actionable suggestions with specific content
- - Include rationale with references to session events
- - Prioritize high-value learnings (frequent corrections > one-time fixes)
- - Be concise—extract the essence, not verbose explanations
-
-## What to Look For
-
-### High-Priority Learnings
-
-1. **Direct Corrections**
- - "No, you should..." / "Actually, we do it this way..."
- - "Don't use X, use Y instead"
- - Explanations of why something was wrong
-
-2. **Failed → Success Patterns**
- - Command failed, then worked with different syntax
- - Tool used incorrectly, then correctly
- - Approach abandoned for better alternative
-
-3. **Project Conventions**
- - File naming patterns
- - Code organization rules
- - Testing practices
- - Build/deployment workflows
-
-4. **Permissions**
- - Commands that required approval
- - Patterns in approved commands (could become wildcards)
-
-### Lower-Priority (Include if Substantial)
-
-5. **Preferences**
- - Formatting styles
- - Communication preferences
- - Tool preferences
-
-6. **Domain Knowledge**
- - Project-specific terminology
- - Architecture decisions
- - Integration patterns
-
-## Output Format
-
-Present suggestions in this format:
-
----
-
-## Suggestion [N]: [Brief descriptive title]
-
-**Type**: `Rule` | `Skill` | `CLAUDE.md` | `Permission` | `Agent`
-**File**: `[exact file path]`
-**Action**: `Add` | `Modify` | `Create`
-
-**Rationale**:
-[1-2 sentences explaining what happened in the session that led to this suggestion]
-
-**Proposed Content**:
-```
-[Exact content to add, properly formatted for the target file]
-```
-
-**Location in file**: [Where to insert: "After section X" or "In permissions.allow array" or "New file"]
-
----
-
-## Workflow
-
-1. **Review Context**: Carefully read through the full session transcript
-2. **Identify Learnings**: Note all potential improvements as you read
-3. **Explore Config**: Use Glob and Read to examine existing Claude configuration
-4. **Deduplicate**: Check if learnings are already documented
-5. **Prioritize**: Focus on generalizable, high-impact learnings
-6. **Format**: Present suggestions in the structured format above
-7. **Await Approval**: Present all suggestions and wait for user to accept/reject each
-8. **Implement**: After approval, use Edit tool to make the changes
-
-## Key Principles
-
-- **Be selective**: Not every correction needs documentation—focus on generalizable learnings
-- **Be precise**: Include exact file paths and content, ready to apply
-- **Be concise**: Extract the essence, don't pad with unnecessary explanation
-- **Respect existing structure**: Add to existing files when appropriate
-- **Provide context**: Always explain WHY this should be added (reference session events)
-
-## Example Suggestions
-
-### Example 1: Rule Addition
-
-```markdown
-## Suggestion 1: Always use yarn workspace for tests
-
-**Type**: `Rule`
-**File**: `/path/to/yarn-project/.claude/rules/testing.md`
-**Action**: `Modify` (or `Create` if file doesn't exist)
-
-**Rationale**:
-User corrected me twice when I tried to cd into a package and run `yarn test`. They explained that `yarn workspace` is preferred.
-
-**Proposed Content**:
-## Test Execution
-
-Always use `yarn workspace` to run tests rather than changing directories:
-
-\`\`\`bash
-# Good
-yarn workspace @aztec/archiver test src/archiver.test.ts
-
-# Bad - don't do this
-cd archiver && yarn test src/archiver.test.ts
-\`\`\`
-
-**Location in file**: After "## Test Logging" section (or as new file)
-```
-
-### Example 2: Permission Addition
-
-```markdown
-## Suggestion 2: Pre-approve wc command
-
-**Type**: `Permission`
-**File**: `/path/to/yarn-project/.claude/settings.json`
-**Action**: `Modify`
-
-**Rationale**:
-I requested permission to run `wc -l` three times during log analysis. This is a safe, read-only command.
-
-**Proposed Content**:
-"Bash(wc:*)"
-
-**Location in file**: Add to `permissions.allow` array
-```
-
-Remember: Your goal is to help Claude Code get better over time by capturing the wisdom from each session. Focus on learnings that will help in future, similar situations.
diff --git a/.claude/bootstrap.sh b/.claude/bootstrap.sh
new file mode 100755
index 000000000000..021f5270f3fb
--- /dev/null
+++ b/.claude/bootstrap.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# Bootstrap + test entry for the .claude/ tooling directory. Mirrors the shape
+# used by ci3/bootstrap.sh: emits test commands via `test_cmds`, runs them via
+# `test`. Keeps hook scripts and their tests as a self-contained component.
+source $(git rev-parse --show-toplevel)/ci3/source_bootstrap
+
+hash=$(cache_content_hash ^.claude)
+
+function test_cmds {
+ # source_base cd's us into .claude/, so glob relative-to-here, but emit paths
+ # relative to the git root (same convention used by ci3/bootstrap.sh).
+ for f in tests/*; do
+ [[ -x "$f" ]] || continue
+ echo "$hash ./.claude/$f"
+ done
+}
+
+function test {
+ echo_header ".claude tests"
+ test_cmds | filter_test_cmds | parallelize
+}
+
+case "$cmd" in
+ "")
+ test
+ ;;
+ *)
+ default_cmd_handler "$@"
+ ;;
+esac
diff --git a/.claude/scripts/format-file.sh b/.claude/scripts/format-file.sh
new file mode 100755
index 000000000000..37ff1b21519f
--- /dev/null
+++ b/.claude/scripts/format-file.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# PostToolUse hook: format files after Edit/Write by dispatching to the right
+# formatter based on extension. Reads Claude Code's hook JSON from stdin.
+#
+# Never fails the edit — prints hints to stderr on missing tools and exits 0.
+
+set -u
+
+input=$(cat)
+file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
+
+[[ -z "$file" ]] && exit 0
+[[ ! -f "$file" ]] && exit 0
+
+hint() { printf 'format-file.sh: %s\n' "$*" >&2; }
+
+case "$file" in
+ *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.json)
+ root=""
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ -f "$CLAUDE_PROJECT_DIR/yarn-project/package.json" ]]; then
+ root="$CLAUDE_PROJECT_DIR"
+ else
+ root=$(git -C "$(dirname "$file")" rev-parse --show-toplevel 2>/dev/null || true)
+ fi
+ if [[ -n "$root" && -x "$root/yarn-project/node_modules/.bin/prettier" ]]; then
+ "$root/yarn-project/node_modules/.bin/prettier" --write --log-level=warn "$file" \
+ || hint "prettier failed on $file"
+ else
+ hint "prettier not found — run yarn-project bootstrap to enable format-on-edit"
+ fi
+ ;;
+ *.cpp|*.cxx|*.cc|*.hpp|*.hxx|*.h)
+ cf=""
+ if command -v clang-format-20 >/dev/null 2>&1; then
+ cf=clang-format-20
+ elif [[ -x /opt/homebrew/opt/llvm/bin/clang-format ]] && /opt/homebrew/opt/llvm/bin/clang-format --version 2>/dev/null | grep -q 'version 20'; then
+ cf=/opt/homebrew/opt/llvm/bin/clang-format
+ elif [[ -x /usr/local/opt/llvm/bin/clang-format ]] && /usr/local/opt/llvm/bin/clang-format --version 2>/dev/null | grep -q 'version 20'; then
+ cf=/usr/local/opt/llvm/bin/clang-format
+ fi
+ if [[ -n "$cf" ]]; then
+ "$cf" -i "$file" || hint "$cf failed on $file"
+ else
+ hint "clang-format 20 not found — install via 'apt install clang-format-20' (Linux) or 'brew install llvm' (macOS)"
+ fi
+ ;;
+ *.rs)
+ # rustfmt walks up from the file path to find .rustfmt.toml, which pins
+ # edition and style. Don't pass --edition here; it would override that.
+ if command -v rustfmt >/dev/null 2>&1; then
+ rustfmt "$file" || hint "rustfmt failed on $file"
+ else
+ hint "rustfmt not found — install via 'rustup component add rustfmt'"
+ fi
+ ;;
+ *.sol)
+ if command -v forge >/dev/null 2>&1; then
+ (cd "$(dirname "$file")" && forge fmt "$file") || hint "forge fmt failed on $file"
+ else
+ hint "forge not found — install foundry via 'curl -L https://foundry.paradigm.xyz | bash && foundryup'"
+ fi
+ ;;
+ *.nr)
+ # nargo fmt operates on whole crates, not individual files.
+ hint "nargo fmt is crate-scoped — run 'nargo fmt' from the noir project directory before committing"
+ ;;
+esac
+
+exit 0
diff --git a/.claude/settings.json b/.claude/settings.json
index 678f0a41a0d0..1cbb77f766c4 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -10,6 +10,17 @@
}
]
}
+ ],
+ "PostToolUse": [
+ {
+ "matcher": "Edit|Write|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "bash -c 'GITROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; [ -n \"$GITROOT\" ] && exec \"$GITROOT/.claude/scripts/format-file.sh\"; exit 0'"
+ }
+ ]
+ }
]
}
}
diff --git a/.claude/tests/agents_symlink_test b/.claude/tests/agents_symlink_test
new file mode 100755
index 000000000000..3c0ef5f26540
--- /dev/null
+++ b/.claude/tests/agents_symlink_test
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# Regression test for the layout rule: every subdir that owns its own .claude/
+# must symlink agents/ to the repository root's .claude/agents/. Without the
+# symlink, Claude Code's upward-walk stops at the subdir .claude/ and silently
+# shadows every root agent.
+#
+# Only agents/ is checked — not skills/. Skills are intentionally per-subdir
+# (the root has repo-wide workflow skills, yarn-project has TS-specific ones,
+# etc.), and letting a subdir shadow root skills/ is the designed behavior.
+# Agents, by contrast, are universal and need to be visible from every cwd.
+#
+# Runs from any cwd.
+
+set -uo pipefail
+
+ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)
+
+RED=$'\033[0;31m'
+GREEN=$'\033[0;32m'
+NC=$'\033[0m'
+
+FAIL=0
+CHECKED=0
+
+fail() { echo "${RED}✗${NC} $1"; ((FAIL++)) || true; }
+pass() { echo "${GREEN}✓${NC} $1"; ((CHECKED++)) || true; }
+
+ROOT_AGENTS="$ROOT/.claude/agents"
+[[ -d "$ROOT_AGENTS" ]] || { fail "root .claude/agents missing at $ROOT_AGENTS"; exit 1; }
+
+# Every subdir .claude/ (excluding the root one) must expose an agents entry
+# that resolves to the same inode as the root agents/ directory.
+while IFS= read -r dir; do
+ [[ "$dir" == "$ROOT/.claude" ]] && continue
+ case "$dir" in
+ "$ROOT"/node_modules/*|*"/node_modules/"*) continue;;
+ "$ROOT"/noir/noir-repo/*) continue;;
+ esac
+ agents="$dir/agents"
+ if [[ ! -e "$agents" ]]; then
+ # Build the relative target (e.g. ../../.claude/agents) with POSIX tools
+ # so the hint is correct on macOS + Linux alike.
+ rel_root=${dir#$ROOT/}
+ depth=$(awk -F/ '{print NF}' <<<"$rel_root")
+ up=$(printf '../%.0s' $(seq 1 "$depth"))
+ fail "${dir#$ROOT/} has no agents/ — add symlink: (cd ${dir#$ROOT/} && ln -s ${up}.claude/agents agents)"
+ continue
+ fi
+ if [[ ! -L "$agents" ]]; then
+ fail "${dir#$ROOT/}/agents is not a symlink (must point at root .claude/agents/)"
+ continue
+ fi
+ # Resolve the symlink target and confirm it matches root agents.
+ resolved=$(cd "$(dirname "$agents")" && cd "$(readlink "$agents")" 2>/dev/null && pwd -P) || resolved=""
+ if [[ "$resolved" != "$ROOT_AGENTS" ]]; then
+ fail "${dir#$ROOT/}/agents resolves to $resolved, expected $ROOT_AGENTS"
+ continue
+ fi
+ pass "${dir#$ROOT/}/agents"
+done < <(find "$ROOT" -type d -name .claude -not -path "$ROOT/noir/*" -not -path "$ROOT/**/node_modules/*")
+
+echo
+if (( FAIL == 0 )); then
+ echo "${GREEN}All $CHECKED subdir .claude/ directories expose agents/ correctly.${NC}"
+ exit 0
+else
+ echo "${RED}$FAIL subdir .claude/ directories are missing or have broken agents symlinks.${NC}"
+ exit 1
+fi
diff --git a/.claude/tests/format_file_test b/.claude/tests/format_file_test
new file mode 100755
index 000000000000..687e3ad393a3
--- /dev/null
+++ b/.claude/tests/format_file_test
@@ -0,0 +1,174 @@
+#!/usr/bin/env bash
+# Test suite for .claude/scripts/format-file.sh. Exercises every language
+# dispatch branch plus the TS fallback-install path. Mirrors ci3/tests/ style.
+set -uo pipefail
+
+# Resolve ROOT from the script's own location so the suite runs from any cwd.
+ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)
+SCRIPT="$ROOT/.claude/scripts/format-file.sh"
+
+# Tests write temp files to /tmp which is outside the repo, so export
+# CLAUDE_PROJECT_DIR to give format-file.sh a known root without relying on
+# upward git walks from the edited file's directory.
+export CLAUDE_PROJECT_DIR="$ROOT"
+
+RED=$'\033[0;31m'
+GREEN=$'\033[0;32m'
+NC=$'\033[0m'
+
+PASS=0
+FAIL=0
+
+pass() { echo "${GREEN}✓${NC} $1"; ((PASS++)) || true; }
+fail() {
+ echo "${RED}✗${NC} $1"
+ [[ $# -ge 2 ]] && echo " $2"
+ if [[ -s /tmp/format_test_stderr ]]; then
+ sed 's/^/ stderr: /' /tmp/format_test_stderr
+ fi
+ ((FAIL++)) || true
+}
+
+# run_hook : invoke format-file.sh with a hook-shaped stdin payload.
+run_hook() {
+ local f=$1
+ printf '{"tool_input":{"file_path":"%s"}}' "$f" | "$SCRIPT" 2>/tmp/format_test_stderr
+}
+
+# expect_formatted