[BUG] managed-settings.json "hooks" key is silently ignored (PreToolUse hooks never registered)

Status Open
Reported on v2.1.197
Maintainer reply None cached
Activity 0 comments · opened Sep 30, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

hooks defined in /etc/claude-code/managed-settings.json (the OS-level "managed policy" tier) are silently never loaded into the hook registry, so PreToolUse hooks configured there never fire — even though the same file's other keys (e.g. permissions) are correctly loaded and applied. There is no error, warning, or log line indicating the hooks key was rejected or skipped; debug logs show the hook registry as permanently empty for the whole session.

This defeats the intended purpose of managed settings for hook-based guardrails: it's the one config tier that's supposed to be enforceable regardless of what the agent or an interactive user does, and organizations deploying credential-protection hooks (e.g. blocking reads of files matching .aiignore patterns) there will have those hooks silently do nothing, with no signal that anything is wrong.

Environment

  • Claude Code version: 2.1.197
  • Platform: Docker container, overlay2 rootfs, /.dockerenv present (confirmed via /proc/1/cgroup)
  • Kernel reports 5.15.153.1-microsoft-standard-WSL2 (inherited from the Docker Desktop/WSL2 host engine — the container itself has no WSL interop: no WSLInterop in /proc/sys/fs/binfmt_misc, no reg.exe/powershell.exe on $PATH, no /mnt/c)
  • CLAUDE_CONFIG_DIR=/home/devagent/.claude
  • Reproduced identically both in the live interactive session and in a fresh, isolated claude -p headless invocation (ruling out session-specific state)

Steps to reproduce

  1. Deploy a managed-settings file at /etc/claude-code/managed-settings.json (root-owned, mode 644):

{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write|NotebookEdit|Grep|Glob|Bash",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["/etc/claude-code/hooks/aiignore-guard.js"]
}
] } ] }}

  1. Deploy the referenced hook script at /etc/claude-code/hooks/aiignore-guard.ng .aiignore patterns, gitignore-style). The script itself is correct —verified independently by piping a matching Read tool-call payload into it dirly returns:{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny"Blocked by .aiignore (...): '...' matches an ignored pattern. ..."}}3. Start a session (interactive, or headless via -p) and have Claude Read a fipattern the hook is meant to block, e.g.:claude -p "Read the file <path-matching-.aiignore-pattern>" --allowedTools "Re --debug hooks --debug-file /tmp/hook-debug.logExpected behaviorThe Read tool call is denied by the PreToolUse hook, per the deny decision thevoked directly.Actual behaviorThe file is read successfully; its full contents (including embedded secrets, ase connection string) are returned in the response. The hooks-category debuglog shows the hook registry as empty for the entire process lifetime:[DEBUG] Hooks: Found 0 total hooks in registry (at startup, before settin...[DEBUG] Watching for changes in setting files /home/devagent/.claude/settings.ings.json, /workspace/.claude/settings.local.json,/etc/claude-code/managed-settings.json... [DEBUG] Programmatic settings change notification for policySettings[DEBUG] Settings changed from policySettings, updating app state[DEBUG] Replacing all allow rules for destination 'policySettings' with 0 rule[DEBUG] Replacing all deny rules for destination 'policySettings' with 0 rule([DEBUG] Replacing all ask rules for destination 'policySettings' with 0 rule(s...[DEBUG] Hooks: Found 0 total hooks in registry (again, ~2.4s later, immed call)Notably, policySettings (i.e. managed-settings.json) is recognized and its chaonly its permissions sub-key appears to be extracted (correctly showing 0rules, since our file defines none). There is no equivalent log line anywhere policySettings/managed-settings.json, in contrast to localSettings (projectsettings.local.json), where all 29 configured allow-rules are logged by name dests the settings loader for the managed/policy tier does not currently readthe hooks key at all, unlike project- and user-level settings.json, where (perected to work.Impact- Organizations relying on /etc/claude-code/managed-settings.json to deploy hoal-access prevention, audit logging, command blocking, etc.) get no enforcementand no error — the failure is completely silent from the product's perspective- The alternative — defining hooks in project/user settings.json — is not an eiles are owned by, and writable by, the same OS user the agent runs as, so ahook placed there is not tamper-resistant against the agent editing it out. /egs.json is presumably meant to close exactly that gap, but doesn't currentlydeliver it for hooks.Suggested fix- Load hooks from /etc/claude-code/managed-settings.json (and platform equivals is loaded from it today.- At minimum, log a warning when a managed-settings file contains a hooks key s doesn't fail silently.Additional note (related, lower severity)Separately, this same debug run surfaced: WSL IDE lockfile path detection failfile or directory, scandir '/mnt/c/Users' — confirming the binary activelyprobes Windows-side paths when it detects a WSL-flavored kernel string, even i have no actual WSL interop. Not the cause of the hooks issue above, but worthflagging as a related edge case.

What Should Happen?

hook from /etc/claude-code/managed-settings.json should be registered

Error Messages/Logs

Steps to Reproduce

Minimal, self-contained repro — no custom hook script needed, uses a one-liner sh command that unconditionally denies:

  1. Create /etc/claude-code/managed-settings.json (root-owned, mode 644):

``json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "sh",
"args": ["-c", "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"blocked by managed policy\"}}'"]
}
]
}
]
}
}
``

  1. Run a headless probe against any readable file, with hooks debug logging on:

``
claude -p "Read /etc/hostname" --allowedTools "Read" \
--debug hooks --debug-file /tmp/hook-debug.log
`
Expected: the
Read is denied, reason "blocked by managed policy".
Actual: the read succeeds;
/etc/hostname's contents are returned. grep -i hook /tmp/hook-debug.log shows Hooks: Found 0 total hooks in registry`.

  1. Isolating control: move the exact same hooks block (unchanged) from /etc/claude-code/managed-settings.json into ~/.claude/settings.json (user tier) and repeat step 2. This time the Read is correctly denied — proving the hook mechanism itself, the matcher syntax, and the command hook contract are all fine, and the defect is specifically in the managed/policy settings tier not surfacing its hooks key to the registry.

(Our original finding used a real-world hook — .aiignore-pattern enforcement blocking reads of files like appsettings.json — which independently confirmed the same defect; happy to share that script too if useful for a more realistic reproduction.)

Expected behavior

The Read tool call is denied by the PreToolUse hook, per the deny decision the hook script produces when invoked directly.

Actual behavior

The file is read successfully; its full contents (including embedded secrets, in our case a plaintext database connection string) are returned in the response. The hooks-category debug log shows the hook registry as empty for the entire process lifetime:

[DEBUG] Hooks: Found 0 total hooks in registry      (at startup, before settings finish loading)
...
[DEBUG] Watching for changes in setting files /home/devagent/.claude/settings.json, /workspace/.claude/settings.json, /workspace/.claude/settings.local.json, /etc/claude-code/managed-settings.json...
[DEBUG] Programmatic settings change notification for policySettings
[DEBUG] Settings changed from policySettings, updating app state
[DEBUG] Replacing all allow rules for destination 'policySettings' with 0 rule(s): []
[DEBUG] Replacing all deny rules for destination 'policySettings' with 0 rule(s): []
[DEBUG] Replacing all ask rules for destination 'policySettings' with 0 rule(s): []
...
[DEBUG] Hooks: Found 0 total hooks in registry      (again, ~2.4s later, immediately around the actual tool call)

Notably, policySettings (i.e. managed-settings.json) is recognized and its change event is processed — but only its permissions sub-key appears to be extracted (correctly showing 0 rules, since our file defines none). There is no equivalent log line anywhere for hooks being parsed out of policySettings/managed-settings.json, in contrast to localSettings (project settings.local.json), where all 29 configured allow-rules are logged by name during the samerun. This suggests the settings loader for the managed/policy tier does not currently read the hooks key at all, unlike project- and user-level settings.json, where (per existing docs) hooks are expected to work.

Impact

  • Organizations relying on /etc/claude-code/managed-settings.json to deploy hook-based guardrails (credential-access prevention, audit logging, command blocking, etc.) get no enforcement and no error — the failure is completely silent from the product's perspective. We only found it by manually replaying the hook's stdin payload and by comparing debug-log output line by line.
  • The alternative — defining hooks in project/user settings.json — is not an equivalent substitute for this use case: those files are owned by, and writable by, the same OS user the agent runs as, so a hook placed there is not tamper-resistant against the agent (or anything running as that account) editing it out. /etc/claude-code/managed-settings.json is the only tier that is not writable by the agent's own uid in a typical deployment, which is presumably why it exists as a separate concept from project/user settings — but it doesn't currently deliver that guarantee for hooks specifically.

Suggested fix

  • Load hooks from /etc/claude-code/managed-settings.json (and platform equivalents, e.g. Windows registry/C:\Program Files\ClaudeCode\managed-settings.json per the WSL policy-chain logic referenced elsewhere in the binary) the same way permissions is loaded from it today.
  • At minimum, log a warning when a managed-settings file contains a hooks key that is being ignored, so this doesn't fail silently.

Additional note (related, lower severity)

Separately, this same debug run surfaced: WSL IDE lockfile path detection failed (ENOENT): ENOENT: no such file or directory, scandir '/mnt/c/Users' — confirming the binary actively probes Windows-side paths when it detects a WSL-flavored kernel string, even in containers (like ours) that have no actual WSL interop (/mnt/c doesn't exist, no WSLInterop binfmt entry). Not the cause of the hooks issue above, but worth being aware of as a related WSL-detection edgecase for containerized environments running on a WSL2-hosted Docker Desktop backend.

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.197 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗