Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .claude/README.md
Original file line number Diff line number Diff line change
@@ -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/`.
179 changes: 0 additions & 179 deletions .claude/agents/retrospective.md

This file was deleted.

30 changes: 30 additions & 0 deletions .claude/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions .claude/scripts/format-file.sh
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
]
}
]
}
}
69 changes: 69 additions & 0 deletions .claude/tests/agents_symlink_test
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +2 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Does this apply to other folders as well, like skills?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would cause skills to be loaded multiple times I think. They are loaded dynamically just fine

#
# 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
Loading
Loading