FileChanged hook stops firing after any cwd change in the session; CwdChanged watchPaths does not restore it (2.1.270)

Status Open
Reported on v2.1.270
Maintainer reply None cached
Activity 1 comment · opened Sep 18, 2026

Environment

  • Claude Code CLI 2.1.270, run by the Claude desktop app 1.52386.6 (macOS, Darwin 25.5.0), --input-format stream-json, project settings in <cwd>/.claude/settings.json.
  • Hook config (documented shape, hooks.md §FileChanged):

``json
{"hooks":{"FileChanged":[{"matcher":"test.bell","hooks":[{"type":"command","command":"/Users/me/doorbell-test/ring.sh","asyncRewake":true}]}]}}
`
ring.sh` logs each invocation, prints new lines to stderr and exits 2 when the file grew (else 0).

Expected (docs)

  • hooks.md §FileChanged: runs "no matter what changed the file … a process outside Claude Code entirely"; matcher segments are "literal filename in the working directory".
  • hooks.md async constraints: "an asyncRewake hook that exits with code 2 wakes Claude immediately even when the session is idle."
  • hooks.md §CwdChanged output: watchPaths "Replaces the current dynamic watch list. Paths from your matcher configuration are always watched."

Observed

| Step | Action (external process appends one line to test.bell with O_APPEND) | Hook ran? | Session woke? |
|---|---|---|---|
| A | session idle, fresh | yes, +1 s | yes, +4 s |
| B | session mid sleep 120 | yes, same second | yes, 6 s after the turn ended |
| D | session first ran one Bash command cd /tmp && ls >/dev/null && pwd, then idle | no | no (120 s) |
| E | session then ran cd <original cwd> && pwd, idle | no | no |
| F0 | fresh session with an added CwdChanged hook that returns {"watchPaths":["<abs path to test.bell>"]} | yes | yes |
| F | same session ran cd /tmp && …; the CwdChanged hook ran and returned the absolute path | no | no |

After any directory change in the main conversation, the FileChanged watcher never fires again for the session — not for the matcher path (contradicting "always watched"), and not for a watchPaths re-pin from a CwdChanged hook. The session never receives any notice that the watch was lost.

Minimal repro

  1. mkdir ~/fc && cd ~/fc && mkdir .claude && touch bell ; settings above with matcher bell and a hook echo hit >> ~/fc/log; exit 2 with asyncRewake: true.
  2. Start a session in ~/fc; from another terminal echo x >> ~/fc/bell → hook runs, session wakes.
  3. In the session, run Bash cd /tmp && pwd. From the other terminal echo y >> ~/fc/bell → nothing, ever.

Impact

A FileChanged+asyncRewake doorbell is the only documented way for an outside process to wake an idle interactive session. Any cd inside a Bash command silently disarms it, so real sessions (which cd within their first minute) never see it work, and conclude the feature is broken. We lost ~12 h of teammate messages on 2026-09-18 to this before measuring it.

Asks

  1. Keep matcher paths watched across CwdChanged (as documented), or re-resolve them against the new cwd and say so.
  2. Honour watchPaths returned by a CwdChanged hook after the change.
  3. Emit a visible notice when the watch list becomes empty/dead.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗