claude-code - 💡(How to fix) Fix Hook hot-reload silently breaks when settings.json is replaced (atomic save / git checkout / symlinked dotfiles) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#57852Fetched 2026-05-11 03:23:41
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
0
Timeline (top)
labeled ×4closed ×1commented ×1

Removing a hook from ~/.claude/settings.json is not picked up by a running session — every subsequent tool call keeps showing:

PreToolUse:Bash hook error Failed with non-blocking status code: /bin/sh: 1: /root/.claude/hooks/tpu-priority-gate.sh: not found

Restarting the session is the only fix. Recreating the symlink (~/.claude/settings.json → dotfiles target) does not help.

Error Message

PreToolUse:Bash hook error Failed with non-blocking status code: /bin/sh: 1: /root/.claude/hooks/tpu-priority-gate.sh: not found

Root Cause

Root cause (confirmed via /proc and inotify inspection)

Fix Action

Fix / Workaround

Workaround (for users hitting this)

Code Example

PreToolUse:Bash hook error Failed with non-blocking status code: /bin/sh: 1: /root/.claude/hooks/tpu-priority-gate.sh: not found

---

$ cat /proc/<cc_pid>/fdinfo/<inotify_fd> | grep <inode>
inotify wd:75 ino:bc4249 sdev:10300005 mask:4000c82 ...

---

T=~/.claude/settings.json
cp $T $T.tmp
# remove a hook from $T.tmp
mv $T.tmp $T            # atomic rename — watch is now dead
RAW_BUFFERClick to expand / collapse

Summary

Removing a hook from ~/.claude/settings.json is not picked up by a running session — every subsequent tool call keeps showing:

PreToolUse:Bash hook error Failed with non-blocking status code: /bin/sh: 1: /root/.claude/hooks/tpu-priority-gate.sh: not found

Restarting the session is the only fix. Recreating the symlink (~/.claude/settings.json → dotfiles target) does not help.

Environment

  • Claude Code 2.1.128-dev.20260504.t010211.sha46e7d1c (also reproducible on 2.1.134-dev)
  • Linux x86_64, Node v22.22.2
  • ~/.claude/settings.json is a symlink to a dotfiles-managed file (/root/src/dotfiles/users/<user>/.claude/settings.json)
  • The settings file was edited with a tool that does an atomic save (write tempfile → rename() over target). This is what vim, VS Code, git checkout, and Claude Code's own Write/Edit tools all do.

Root cause (confirmed via /proc and inotify inspection)

When CC starts, it sets up an inotify watch on the resolved inode of the settings file:

$ cat /proc/<cc_pid>/fdinfo/<inotify_fd> | grep <inode>
inotify wd:75 ino:bc4249 sdev:10300005 mask:4000c82 ...

Mask 0x4000c82 = IN_EXCL_UNLINK | IN_MOVE_SELF | IN_DELETE_SELF | IN_MOVED_TO | IN_MODIFY. Notably IN_ATTRIB (0x004) is not set and the parent directory is not watched.

When the user edits the file via atomic save, rename() clobbers the target:

  • The original inode's hard-link count drops to 0. The kernel fires IN_ATTRIB on the old inode (link-count change, see fsnotify_link_count() in vfs_rename()). IN_ATTRIB is not in CC's mask → CC receives nothing.
  • IN_DELETE_SELF does not fire because the inode is kept alive by the inotify watch + a leaked open read-only fd inside CC (/proc/<cc_pid>/fd/185 -> .../settings.json (deleted)). It only fires when the inode is finally evicted, which never happens while the session is alive.
  • IN_MOVED_TO/IN_CREATE would fire on the parent directory — but the parent dir isn't watched.

Result: CC's watch is permanently attached to a dead inode (bc4249) while the live file is at bc31ae. The watch never fires again.

Recreating the symlink doesn't help because the watch is on the target file's inode, not the symlink, and not the directory. ~/.claude/ directory isn't watched either.

Reproduction without the symlink

This is not symlink-specific. Any atomic-save replacement of a watched settings file breaks it:

T=~/.claude/settings.json
cp $T $T.tmp
# remove a hook from $T.tmp
mv $T.tmp $T            # atomic rename — watch is now dead

Additional observation

Even forcing an IN_MODIFY on the dead inode (via cat new.json > /proc/<pid>/fd/<n>, which the kernel does deliver — verified with a standalone inotify test) does not cause CC to refresh its hook registry. The next tengu_run_hook event still shows numCommands:5 (3 user hooks from the old config + 2 plugin hooks) instead of the expected 3. So even when the watcher DOES fire, the hook registry doesn't refresh.

Suggested fix

Any of:

  1. Watch the parent directory of each settings file in addition to the file itself, and re-establish the file watch when an IN_MOVED_TO/IN_CREATE matching the settings filename fires.
  2. Add IN_ATTRIB to the watch mask and re-stat/re-watch the path when the watched inode's link count changes.
  3. Use polling (fs.watchFile() / stat-based) for settings files specifically — they're small and rarely change, so the cost is negligible and polling always re-resolves the path.
  4. Add a manual /reload (or hook reload to /hooks) so users can force a refresh without losing running monitors / background tasks.

Workaround (for users hitting this)

Restart the session. There is no in-session way to force a settings reload.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Hook hot-reload silently breaks when settings.json is replaced (atomic save / git checkout / symlinked dotfiles) [1 comments, 2 participants]