[Bug] Plugin-registered CwdChanged and FileChanged hooks don't fire (settings.json works) — v2.1.153
[Bug] CwdChanged and FileChanged hooks never fire in v2.1.153
Summary
Neither CwdChanged nor FileChanged hooks fire under the conditions described in the hooks docs. SessionStart from the same plugin works fine, so plugin loading itself is not the issue.
This appears to be a continuation of the unresolved #44925 (closed stale, same use case: re-applying mise env on cwd/file changes).
Environment
- Claude Code
2.1.153 - macOS (Darwin 25.5.0)
- Plain terminal — no cmux, no IDE extension, no managed-settings policy
- Plugin installed via local marketplace, enabled in
~/.claude/settings.json
Plugin hooks/hooks.json
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/apply-mise-env.sh\"" }] }
],
"CwdChanged": [
{ "hooks": [{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/apply-mise-env.sh\"" }] }
],
"FileChanged": [
{
"matcher": "mise.toml",
"hooks": [{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/apply-mise-env.sh\"" }]
}
]
}
}
The hook script logs every invocation (stdin + timestamp + pid) to a tmp file before doing anything else, so we can see every fire unconditionally.
Reproduction
- Start Claude in a project directory containing
mise.toml. SessionStartfires — log entry appears. ✅- Ask Claude to
cd ./subdir(a directory inside the project). The Bash tool reports the new cwd and it persists across subsequent Bash calls — this matches the exact example in theCwdChangeddocs (/Users/my-project→/Users/my-project/subdir). NoCwdChangedlog entry appears. ❌ - Externally edit
mise.toml(mtime updates). NoFileChangedlog entry appears. ❌
Repeated across multiple fresh claude startups and /resumes. The hook script is never invoked for either event.
What I checked
- Hook JSON shape matches docs (matcher-wrapped form for SessionStart works; same shape for CwdChanged;
matcher: "mise.toml"per the docs' literal-filename syntax for FileChanged). - Plugin is loaded (SessionStart proves this).
$CLAUDE_ENV_FILEmachinery in the hook script is irrelevant — the script writes the log entry before anything else, so even a no-op hook would show up.- No managed settings restricting hooks.
- Version 2.1.153 is well past 2.1.83 which added these events; no CHANGELOG entries since suggest any bug fix to them.
- #44925 reports the same symptom with the same use case (mise env reload) and was closed stale without developer comment.
Expected
Per docs, CwdChanged should fire when Claude executes cd (the docs' own example matches the test), and FileChanged should fire when a file matched by the matcher is modified on disk.
Actual
Neither fires. Only SessionStart from the same plugin definition produces hook invocations.
4 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Follow-up: narrowed to plugin-installed hooks specifically
Further testing isolates the bug to plugin-installed
hooks/hooks.jsonforCwdChangedandFileChanged. The same hooks registered in.claude/settings.jsonwork fine.Test matrix (same script, same machine, v2.1.153)
| Registration | SessionStart | CwdChanged | FileChanged |
|-----------------------------------------------|--------------|---------------|---------------|
| Plugin
hooks/hooks.jsononly | ✅ | ❌ | ❌ || Project
.claude/settings.jsononly | ✅ | ✅ | ✅ || Both | ✅ (2 fires) | ✅ (2 fires) | ✅ (2 fires) |
In the "both" case, every
cdand everymise.tomledit produces two log entries at the same timestamp with the same payload but different pids — i.e., both registrations fire. So the plugin handler can be invoked, it just isn't unless asettings.jsonregistration for the same event exists alongside it. (Best guess: the listener/watcher initialization is skipped for plugin-only registrations, but once any registration creates the listener, the plugin handler gets wired into it.)Things ruled out
settings.json.~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/was indeed missingFileChangedbecause the source had changed since first install, but clearing the cache (rm -rfthe plugin cache dir) and restarting did not change the result forCwdChanged. Note: for asource: directorymarketplace, no cache directory is re-created on restart — Claude appears to load the plugin directly from the marketplace path. Worth a separate look at whetherdirectorymarketplaces invalidate cached plugins when source files change; in our case the cache silently held a stale snapshot until I deleted it.source: startupvssource: resume: tested across both; same result.Reproduction
hooks/hooks.jsonin plugin (alone — no settings.json hooks):mise.toml. SessionStart fires. ✅cd ./subdir(in-project, cwd persists across Bash calls). No CwdChanged. ❌mise.toml. No FileChanged. ❌Now add
.claude/settings.jsonregistering the same three events, pointing at the absolute path of the same script. Restart Claude. Both events fire — twice each (once from plugin, once from settings).Workaround
Register
CwdChangedandFileChangedin.claude/settings.json(or~/.claude/settings.json) instead of, or in addition to, the plugin'shooks/hooks.json.Closing as a duplicate of https://github.com/anthropics/claude-code/issues/16288
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.