Skip to content

Claude Code plugin: governance hooks silently never fire on Windows (empty audit log, no enforcement) #3831

Description

@talosrobotics

Summary

On Windows, the agt-governance Claude Code plugin's SessionStart, UserPromptSubmit, and PreToolUse hooks never execute. The plugin's MCP server and slash commands work, and agt_policy_status reports mode: "enforce" with a healthy prompt-defense grade — but no prompt or tool call is ever evaluated, and the audit log is never written. The enforcement layer is silently absent while the status surface reports it as active.

Because the failure happens in the harness's hook launch, upstream of the plugin's own code, the denyOnPolicyError fail-closed setting cannot catch it.

Environment

  • Windows 11 Pro (10.0.26200), PowerShell 7
  • agt-governance plugin v5.0.0, installed via the plugin marketplace (also reproduces from a checkout with --plugin-dir)
  • Node.js on PATH (the plugin's MCP server runs fine)

Root cause

agent-governance-claude-code/hooks/hooks.json launches every hook through:

"command": "${CLAUDE_PLUGIN_ROOT}/bin/agt-node"

bin/agt-node is an extensionless POSIX sh script (exec node "$@"). Because the hook definition includes args, Claude Code runs it in exec form: the command is spawned directly with no shell (see the hooks documentation, https://code.claude.com/docs/en/hooks — shell form vs exec form). On win32 that means the sh script fails to launch (ERROR_BAD_EXE_FORMAT), silently. The bin/agt-node.cmd sibling exists but is never referenced — and an explicit .cmd path does not spawn in exec form either (.cmd/.bat require a shell; Node's child_process.spawn refuses them without shell: true since CVE-2024-27980). Only a command resolvable to an .exe (e.g. node) launches.

Evidence

  1. Session evidence: after a full interactive session on Windows (many prompts and tool calls) with the plugin installed and mode: "enforce", agt_policy_status reported auditEntries: 0 and %USERPROFILE%\.claude\agt\audit-log.json did not exist. Every UserPromptSubmit should have appended an audit entry (lib/policy.mjsrecordAudit).
  2. Hook plumbing works when launched correctly: piping a hostile prompt into hooks/user-prompt-submit.mjs via bin\agt-node.cmd returns decision: "block" and appends a hash-chained audit entry. The bug is only in how the hook is launched.
  3. Probe under a real headless Claude Code session: three UserPromptSubmit hooks registered via settings —
    • extensionless sh script (the plugin's pattern): did not run
    • explicit .cmd script: did not run
    • "command": "node" with the script as an arg: ran
  4. Fix verified: after changing hooks.json to invoke node directly, a claude -p session with --plugin-dir wrote a prompt.submit audit entry as expected.

Suggested fix

Invoke node directly; agt-node is a pure passthrough to node anyway, so this is equivalent on POSIX and makes Windows work:

{
  "type": "command",
  "command": "node",
  "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/user-prompt-submit.mjs"],
  "cwd": "${CLAUDE_PLUGIN_ROOT}",
  "timeout": 30
}

(Same change for session-start.mjs and pre-tool-use.mjs; bin/agt-node{,.cmd} become dead code.)

An alternative is shell form — dropping args and using a single command string ("command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/user-prompt-submit.mjs"), which runs through PowerShell on Windows and sh on POSIX — but exec form with node avoids shell-quoting concerns for paths with spaces and is verified working on both spawn paths.

I have this one-line-per-hook fix verified locally on Windows 11 and can open a PR.

Impact

For a toolkit whose core claim is that denied actions are "structurally impossible", the entire Claude Code enforcement surface being silently inactive on one major OS — while agt-status reports enforce mode as healthy — seems worth both the small fix and a status-surface check (e.g. surface "hooks have never fired this session" in agt_policy_status).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions