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
4 changes: 3 additions & 1 deletion .claude/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
# `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)
# Hash everything agents_symlink_test inspects, not just .claude/: the .codex mirrors and the root
# AGENTS.md/CLAUDE.md symlinks live outside .claude/, so a change there must still rerun the test.
hash=$(cache_content_hash "^.*.claude" "^.*.codex" "^AGENTS.md" "^CLAUDE.md")

function test_cmds {
# source_base cd's us into .claude/, so glob relative-to-here, but emit paths
Expand Down
40 changes: 33 additions & 7 deletions .claude/tests/agents_symlink_test
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,43 @@ while IFS= read -r dir; do
"$ROOT"/noir/noir-repo/*) continue;;
esac
codex="$(dirname "$dir")/.codex"
if [[ ! -L "$codex" ]]; then
fail "${codex#$ROOT/} is missing or is not a symlink to .claude"
rel=${codex#$ROOT/}
# A .codex may either be a symlink to its sibling .claude (the repo root keeps this form), or a
# real directory whose immediate children symlink into .claude (the per-package form, required
# because a sandbox cannot bind-mount a path that is itself a symlink). Either way Codex must see
# exactly the same contents as .claude.
if [[ -L "$codex" ]]; then
resolved=$(cd "$(dirname "$codex")" && cd "$(readlink "$codex")" 2>/dev/null && pwd -P) || resolved=""
claude_resolved=$(cd "$dir" && pwd -P)
if [[ "$resolved" != "$claude_resolved" ]]; then
fail "$rel is a symlink to ${resolved:-?}, expected its sibling .claude ($claude_resolved)"
else
pass "$rel -> .claude"
fi
continue
fi
resolved=$(cd "$(dirname "$codex")" && cd "$(readlink "$codex")" 2>/dev/null && pwd -P) || resolved=""
claude_resolved=$(cd "$dir" && pwd -P)
if [[ "$resolved" != "$claude_resolved" ]]; then
fail "${codex#$ROOT/} resolves to $resolved, expected $claude_resolved"
if [[ ! -d "$codex" ]]; then
fail "$rel is missing (expected a directory mirroring .claude via child symlinks)"
continue
fi
pass "${codex#$ROOT/}"
codex_ok=1
# Forward: every entry in .claude must have a matching symlink in .codex.
while IFS= read -r child; do
name=$(basename "$child")
if [[ ! -L "$codex/$name" || ! "$codex/$name" -ef "$child" ]]; then
fail "$rel/$name should be a symlink to ../.claude/$name"
codex_ok=0
fi
done < <(find "$dir" -mindepth 1 -maxdepth 1)
# Reverse: every entry in .codex must correspond to a .claude entry (no stale or dangling links).
while IFS= read -r entry; do
name=$(basename "$entry")
if [[ ! -e "$dir/$name" && ! -L "$dir/$name" ]]; then
fail "$rel/$name is stale; no matching .claude/$name"
codex_ok=0
fi
done < <(find "$codex" -mindepth 1 -maxdepth 1)
(( codex_ok )) && pass "$rel"
done < <(find "$ROOT" -type d -name .claude -not -path "$ROOT/noir/*" -not -path "$ROOT/**/node_modules/*")

echo
Expand Down
44 changes: 0 additions & 44 deletions yarn-project/precommit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,6 @@ cd $(dirname $0)

export FORCE_COLOR=true

# Verify every .codex directory mirrors its sibling .claude via child symlinks, so that adding a
# file or folder to a .claude config does not silently leave the sandboxed .codex path behind.
# Only immediate children are checked: a symlinked folder (e.g. .codex/skills) already covers its
# contents, and a .codex that is itself a symlink (the repo root) mirrors .claude inherently.
check_codex_symlinks() {
local repo_root claude_dirs claude_dir codex_dir path name
local -a errors=()
repo_root=$(git rev-parse --show-toplevel)
claude_dirs=$(cd "$repo_root" && git ls-files -- '.claude/*' '*/.claude/*' | sed -E 's#(.*/)?\.claude/.*#\1.claude#' | sort -u)

for claude_dir in $claude_dirs; do
codex_dir="${claude_dir%.claude}.codex"
if [ -L "$repo_root/$codex_dir" ]; then
continue
fi
if [ ! -d "$repo_root/$codex_dir" ]; then
errors+=("missing directory $codex_dir (should mirror $claude_dir)")
continue
fi
while IFS= read -r path; do
name=$(basename "$path")
if [ ! -L "$repo_root/$codex_dir/$name" ] || [ ! "$repo_root/$codex_dir/$name" -ef "$path" ]; then
errors+=("$codex_dir/$name should be a symlink to ../.claude/$name")
fi
done < <(find "$repo_root/$claude_dir" -mindepth 1 -maxdepth 1)
while IFS= read -r path; do
name=$(basename "$path")
if [ ! -e "$repo_root/$claude_dir/$name" ] && [ ! -L "$repo_root/$claude_dir/$name" ]; then
errors+=("$codex_dir/$name is stale; no matching $claude_dir/$name")
fi
done < <(find "$repo_root/$codex_dir" -mindepth 1 -maxdepth 1)
done

if (( ${#errors[@]} > 0 )); then
echo -e "\033[31mError:\033[0m .codex directories are out of sync with their .claude siblings:"
for e in "${errors[@]}"; do echo " - $e"; done
echo "Each entry under a .claude folder needs a sibling symlink in .codex, e.g.:"
echo " (cd <component>/.codex && ln -s ../.claude/<name> <name> && git add <name>)"
return 1
fi
}

check_codex_symlinks

# Get all staged files (excluding deleted), relative to yarn-project
staged_files=$(git diff-index --diff-filter=d --relative --cached --name-only HEAD)

Expand Down
Loading