anthropics / anthropics/claude-code
FileChanged hook stops firing after any cwd change in the session; CwdChanged watchPaths does not restore it (2.1.270)
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## 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 `/.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 && pwd`, idle | no | no |
| F0 | fresh session with an added `CwdChanged` hook that returns `{"watchPaths":[""]}` | 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.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with hooks.md §FileChanged and §CwdChanged, then run the minimal repro using the documented matcher and CwdChanged watchPaths. Trace the watcher lifecycle across the Bash cd and verify the matcher and returned absolute path remain effective afterward; done means external appends still invoke the hook and asyncRewake wakes the session.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, shell
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100