Skip to content

fix(#370): hook wrappers silent no-op outside an apexyard fork#371

Merged
atlas-apex merged 2 commits into
devfrom
fix/GH-370-hook-wrappers-silent-noop-outside-fork
May 21, 2026
Merged

fix(#370): hook wrappers silent no-op outside an apexyard fork#371
atlas-apex merged 2 commits into
devfrom
fix/GH-370-hook-wrappers-silent-noop-outside-fork

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the UI-flooding "No such file or directory" error that every .claude/settings.json hook wrapper produced on every tool call when Claude Code was launched outside an apexyard ancestry (e.g. /tmp, a bare project clone, any unrelated repo). Root cause: the walk-up loop correctly stops at / when no anchor file is found, but the wrapper unconditionally execs $r/.claude/hooks/<name>.sh — which lands on /.claude/hooks/<name>.sh (doesn't exist). One error per wrapper, per tool call → unusable terminal.
  • Inserts an anchor-found guard between done; and exec in all 43 wrappers: done;[ -f "$r/.apexyard-fork" ] || [ -f "$r/onboarding.yaml" ] || exit 0;exec .... When neither anchor is found, the wrapper silently exits 0 — the correct behaviour outside an ops fork (hooks are framework-internal and shouldn't fire on unrelated repos).
  • Single uniform transform applied via a python JSON-safe text substitution: 43 OLD → 43 NEW, JSON re-validated post-edit. Zero unguarded done;exec patterns remain in .claude/settings.json.
  • New regression test test_settings_wrappers_silent_noop.sh locks in three invariants: every wrapper has the guard (catches future-added wrapper without the guard), wrapper invoked from /tmp exits 0 silently (empirical bug-fix confirmation), wrapper invoked from inside the fork on a non-trigger path still exits 0 (negative control proving the guard doesn't accidentally block legitimate invocations).

Testing

  • bash .claude/hooks/tests/test_settings_wrappers_silent_noop.sh — PASS at 3/3.
  • Manual reproduction: (cd /tmp && bash -c '<wrapper>') — exits 0 with empty stderr (was: bash: /.claude/hooks/<name>.sh: No such file or directory).
  • In-fork invocation still works: PreToolUse Edit hook on a non-security-sensitive path → exits 0 silently as before.
  • All existing hooks tests still PASS (no regression in the rest of the test suite — the fix is in the wrapper, not in any hook's body).

Glossary

Term Definition
Hook wrapper The bash -c '...' invocation in .claude/settings.json that wraps each .claude/hooks/<name>.sh call. Pre-#370 shape: walk up from $PWD to find an ops-fork anchor, then exec the hook at the resolved path. Post-#370 shape: same walk-up, but if no anchor is found, silently exit 0 instead of attempting to exec at /.
Anchor file One of .apexyard-fork (v2 marker) or onboarding.yaml (v1 legacy). The wrapper resolves the ops-fork root by walking up the directory tree looking for either. Per AgDR-0021 § B + AgDR-0041 § "Decision" point 2 — wrappers accept either form.
Silent no-op outside the fork The corrected behaviour. Adopter runs Claude Code in /tmp or a bare clone, the framework's hooks don't fire (correct — they're framework-internal), and the operator's terminal stays clean. Previously: noise flood per tool call.
Walk-up loop termination at / Bash idiom: while ... && [ "$r" != / ]; do r=${r%/*}; done. When no anchor is found, $r arrives at /. The fix's `

Closes #370

When Claude Code is launched from a directory with no apexyard
ancestry (e.g. /tmp, a bare project clone, or any dir outside the ops
fork), all 43 hook wrappers crashed with "No such file or directory"
on every tool call:

  Failed with non-blocking status code:
  bash: /.claude/hooks/detect-role-trigger.sh: No such file or directory

Root cause: the walk-up loop in each wrapper correctly stops at /
when no anchor file is found, but the wrapper unconditionally execs
$r/.claude/hooks/<name>.sh — landing on /.claude/hooks/<name>.sh
when $r is /. One error per wrapper, per tool call → UI noise flood.

Fix: insert an anchor-found guard between `done;` and `exec`:

  done;[ -f "$r/.apexyard-fork" ] || [ -f "$r/onboarding.yaml" ] || exit 0;exec ...

When neither anchor is found, the wrapper exits 0 silently — the
correct behaviour outside an ops fork (hooks are framework-internal
and shouldn't fire on unrelated repos).

Scope: all 43 wrappers in .claude/settings.json. Applied via a python
JSON-safe text substitution + re-validated the JSON parses cleanly.

Regression test (test_settings_wrappers_silent_noop.sh):

- Invariant 1: every wrapper has the '|| exit 0;exec' guard (no
  unguarded `done;exec` patterns) — protects against a regression
  where a new wrapper added later forgets the guard
- Invariant 2: a representative wrapper invoked from /tmp (no fork
  ancestry) exits 0 with empty stderr — empirical confirmation of
  the bug fix
- Invariant 3: same wrapper invoked from inside the fork on a non-
  trigger path still exits 0 silently — negative control proving
  the guard doesn't accidentally block legitimate in-fork invocations

3/3 PASS at HEAD.

Closes #370

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@atlas-apex atlas-apex left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review: PR #371

Commit: 4b426ab780aca4aa9f4350df477edd0d7155b5c5

Verdict intent: CHANGES REQUESTED. Filed as a comment review because GitHub disallows self---request-changes. The blocking item below is non-advisory and must be resolved before merge.

Summary

Targeted bug fix for the UI-flooding "No such file or directory" noise that every .claude/settings.json hook wrapper produced on every tool call when Claude Code was launched outside an apexyard ancestry. Root cause is correctly diagnosed: walk-up loop terminates at / when no anchor is found, but the wrapper unconditionally execs $r/.claude/hooks/<name>.sh, landing on /.claude/hooks/<name>.sh (doesn't exist). Fix inserts an anchor-found guard ([ -f "$r/.apexyard-fork" ] || [ -f "$r/onboarding.yaml" ] || exit 0) between done; and exec in all 43 wrappers. Scope is exactly 2 files (settings.json + new regression test). The fix is correct, the bug reproduces pre-fix and is silenced post-fix.

Checklist Results

  • Architecture & Design: Pass (single-anchor laxity at wrapper level matches AgDR-0041 § Decision point 2; both-files-required strictness stays in the hook-internal resolver)
  • Code Quality: Pass (uniform transform, JSON re-validated)
  • Testing: Pass with one strengthening suggestion (see below)
  • Security: N/A (no auth/crypto/secrets touched)
  • Performance: Pass (one extra -f test per wrapper invocation, negligible)
  • PR Description & Glossary: Pass (narrative summary bullets, 4-row glossary)
  • Technical Decisions (AgDR):N/A (faithful execution of existing AgDR-0021 + AgDR-0041; no new decision)
  • Adopter Handbooks: N/A (no architecture / migration / language-specific findings)

Issues Found

⛔ BLOCKING: CI shellcheck workflow failing at HEAD 4b426abhttps://github.com/me2resh/apexyard/actions/runs/26239945147

The new regression test trips SC2164 twice:

./.claude/hooks/tests/test_settings_wrappers_silent_noop.sh:71:3: warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164]
./.claude/hooks/tests/test_settings_wrappers_silent_noop.sh:89:3: warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164]

The shellcheck .claude/hooks workflow runs at -S warning, so warnings block. Per .claude/rules/pr-quality.md § "No Red CI Before Merge" — never merge with red CI, even pre-existing or unrelated. Here the failure is new and caused by this PR.

Fix:

# line 71
cd /tmp || exit 1

# line 89
cd "$ROOT" || exit 1

Verification (positive findings)

  1. Uniform transform correctness:

    • grep -cF '|| exit 0;exec' .claude/settings.json → 43 ✓
    • grep -cF 'done;exec' .claude/settings.json → 0 ✓
    • grep -cF 'r=$PWD' .claude/settings.json → 43 ✓
    • Unique JSON-escaped transform shape: 1 ✓
    • No drive-by edits — every +/- in settings.json is wrapper-shape change
  2. JSON validity post-edit: jq . .claude/settings.json > /dev/null exits 0 ✓

  3. Guard semantics: walk-up loop and guard use the same || shape (single-anchor laxity at the wrapper level, both-files-required stays in the in-hook resolver). Matches AgDR-0041 § Decision point 2. Sanity test: injected a .apexyard-fork anchor in /tmp/XXX/, ran the wrapper from there → hook DID run (stderr HOOK_RAN, rc=0). Confirms the guard ALLOWS exec when an anchor exists.

  4. Bug reproduction:

    • Pre-fix shape (no guard) run from /tmp: rc=126, stderr = bash: /.claude/hooks/detect-role-trigger.sh: No such file or directory; bash: line 0: exec: ... cannot execute: No such file or directory
    • Post-fix shape (this PR) run from /tmp: rc=0, empty stderr
    • Fix works as advertised.
  5. Regression test 3/3 PASS locally.

  6. Scope discipline: 2 files, +154/-43. No other edits.

Suggestions

  1. (nit) Strengthen Invariant 3 with a positive in-fork case. Current invariant 3 runs the wrapper inside the fork on a non-trigger path and asserts rc=0 + empty stderr. This passes whether the hook actually ran (silent on non-trigger) OR if the guard accidentally exits 0 always (hook never runs). The 3-invariant suite as a whole is fine — invariant 1 enforces the guard SHAPE textually, which makes the "always exits" failure mode impossible to reach without also tripping invariant 1. Still, an additional positive sub-case ("inside fork on a TRIGGER path → expected banner appears on stderr") would prove end-to-end behaviour rather than relying on the textual shape check. Worth a follow-up, not blocking.

  2. (nit) Test scope: one wrapper as proxy for 42. The bug is in the wrapper SHAPE, not in any individual hook body, and invariant 1 (textual count check) catches the shape regression for all 43. The runtime smoke tests only detect-role-trigger.sh. Acceptable as-is given the uniform transform; a per-wrapper smoke would yield diminishing returns. PR-acceptable.

  3. (suggestion) Wire test_settings_wrappers_silent_noop.sh into CI. The framework's convention is per-test CI wiring (see extract-subpacks-on-release.yml, site-counts-check.yml). Without a CI entry, invariant 1 will only be checked when someone manually runs the test. A small dedicated workflow (or a generic run-hook-tests.yml that globs .claude/hooks/tests/test_*.sh) would close the loop. Not blocking; can ship as a follow-up.

Verdict

CHANGES REQUESTED (filed as comment per self-PR constraint)

The fix itself is correct, well-scoped, and well-tested locally. The blocker is the new red CI status from the SC2164 shellcheck warnings on the test file — fixable in ~30 seconds (|| exit 1 on the two cd calls). Once CI is green at the updated HEAD, this is approval-ready and I'd recommend re-running /code-review on the new commit.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 4b426ab780aca4aa9f4350df477edd0d7155b5c5

Rex review on PR #371 caught two SC2164 warnings on the new test that
the framework's shellcheck CI workflow blocks at `-S warning`. Both
calls were inside subshells (so `cd` failure wouldn't propagate to the
test loop) — but shellcheck doesn't analyse subshell scope. Add
explicit `|| exit 1` guards to satisfy the linter.

Line 71: `cd /tmp || exit 1`
Line 89: `cd "$ROOT" || exit 1`

Test still 3/3 PASS locally.

Refs #370 PR #371 Rex review (CI red blocker).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@atlas-apex atlas-apex left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review: PR #371 (re-review after shellcheck fix)

Commit: f486e4ade431251e473483f7ade8a34a41d1a3b0

Summary

Re-review at new HEAD f486e4a after the shellcheck SC2164 follow-up. Prior review (CHANGES REQUESTED on 4b426ab for CI red on SC2164 warnings at test_settings_wrappers_silent_noop.sh:71 + :89) is now resolved.

Verification against the 5 checks

# Check Status
1 cd ... || exit 1 is the canonical SC2164 fix PASS — `
2 Test still 3/3 PASS PASS — ran bash .claude/hooks/tests/test_settings_wrappers_silent_noop.sh at HEAD f486e4a: 3 passed, 0 failed
3 shellcheck .claude/hooks CI workflow green at HEAD PASS — workflow completed SUCCESS at 2026-05-21T17:03:22Z (was the red blocker on prior HEAD)
4 No drive-by edits — diff is exactly the 2 lines PASS — git diff 4b426ab..f486e4a --stat reports 1 file changed, 2 insertions(+), 2 deletions(-) on test_settings_wrappers_silent_noop.sh; lines 71 + 89 only
5 Original PR main work unchanged PASS — at HEAD: 43 wrappers (r=$PWD count), 43 guarded (|| exit 0;exec count), 0 unguarded (done;exec count). Matches the prior approved-modulo-CI state.

Checklist Results

  • Architecture & Design: Pass (no architectural surface change; pure CI hygiene)
  • Code Quality: Pass
  • Testing: Pass — regression test still asserts the three invariants and now lints clean
  • Security: Pass (no security surface)
  • Performance: Pass (no performance surface)
  • PR Description & Glossary: Pass (inherited from the prior commit; follow-up commit message clearly explains the why)
  • Summary Bullet Narrative: Pass
  • Technical Decisions (AgDR):N/A — pure linter-hygiene fix, no decision surface
  • Adopter Handbooks: N/A — no handbooks loaded for shell-test diffs

Issues Found

None.

Suggestions

None.

Verdict

APPROVED — all three CI checks green, test passes, scope is exactly the 2 lines described, original 43-wrapper work intact. Cleared for merge from the code-review side; operator owns the marker write + per-PR CEO approval.


Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: f486e4ade431251e473483f7ade8a34a41d1a3b0

@atlas-apex atlas-apex merged commit 181c1a4 into dev May 21, 2026
3 checks passed
@atlas-apex atlas-apex deleted the fix/GH-370-hook-wrappers-silent-noop-outside-fork branch May 21, 2026 17:22
me2resh added a commit that referenced this pull request Jun 5, 2026
* fix(#370): hook wrappers silent no-op outside an apexyard fork

When Claude Code is launched from a directory with no apexyard
ancestry (e.g. /tmp, a bare project clone, or any dir outside the ops
fork), all 43 hook wrappers crashed with "No such file or directory"
on every tool call:

  Failed with non-blocking status code:
  bash: /.claude/hooks/detect-role-trigger.sh: No such file or directory

Root cause: the walk-up loop in each wrapper correctly stops at /
when no anchor file is found, but the wrapper unconditionally execs
$r/.claude/hooks/<name>.sh — landing on /.claude/hooks/<name>.sh
when $r is /. One error per wrapper, per tool call → UI noise flood.

Fix: insert an anchor-found guard between `done;` and `exec`:

  done;[ -f "$r/.apexyard-fork" ] || [ -f "$r/onboarding.yaml" ] || exit 0;exec ...

When neither anchor is found, the wrapper exits 0 silently — the
correct behaviour outside an ops fork (hooks are framework-internal
and shouldn't fire on unrelated repos).

Scope: all 43 wrappers in .claude/settings.json. Applied via a python
JSON-safe text substitution + re-validated the JSON parses cleanly.

Regression test (test_settings_wrappers_silent_noop.sh):

- Invariant 1: every wrapper has the '|| exit 0;exec' guard (no
  unguarded `done;exec` patterns) — protects against a regression
  where a new wrapper added later forgets the guard
- Invariant 2: a representative wrapper invoked from /tmp (no fork
  ancestry) exits 0 with empty stderr — empirical confirmation of
  the bug fix
- Invariant 3: same wrapper invoked from inside the fork on a non-
  trigger path still exits 0 silently — negative control proving
  the guard doesn't accidentally block legitimate in-fork invocations

3/3 PASS at HEAD.

Closes #370

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#370): shellcheck SC2164 — guard cd calls in regression test

Rex review on PR #371 caught two SC2164 warnings on the new test that
the framework's shellcheck CI workflow blocks at `-S warning`. Both
calls were inside subshells (so `cd` failure wouldn't propagate to the
test loop) — but shellcheck doesn't analyse subshell scope. Add
explicit `|| exit 1` guards to satisfy the linter.

Line 71: `cd /tmp || exit 1`
Line 89: `cd "$ROOT" || exit 1`

Test still 3/3 PASS locally.

Refs #370 PR #371 Rex review (CI red blocker).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants