Relationships
#902 swamp audit record --from-hook exits non-zero as Claude Code PostToolUse hook despite internal try/catch design
Opened by dmc · 7/1/2026· Shipped 7/1/2026
Description
When swamp audit record --from-hook is configured as a Claude Code PostToolUse/PostToolUseFailure hook, Claude Code intermittently reports a hook error:
PostToolUse:Bash hook error
Failed with non-blocking status code: No stderr outputThe error fires on real PostToolUse hook invocations but the swamp doctor smoke test passes, and every manual simulation of the command (across multiple payload shapes) exits 0.
Environment
- swamp version: 20260701.000413.0-sha.eaa21300
- tool: claude (Claude Code)
- OS: macOS Darwin 25.5.0
Steps to Reproduce
- Run
swamp init --tool claudeto wire the PostToolUse hook in.claude/settings.local.json - Use Claude Code and run any Bash tool call
- Observe the hook error in Claude Code's terminal output
Observed Behavior
Claude Code reports:
PostToolUse:Bash hook error
Failed with non-blocking status code: No stderr outputswamp doctor audit returns overallStatus: "pass" including the recording-smoke-test check. Running swamp audit record --from-hook manually with all tested payload shapes (valid Claude payload, missing tool_input, lowercase tool_name, invalid JSON, empty stdin) always exits 0.
Key Findings from Source Inspection
1. Command source guarantees exit 0 (src/cli/commands/audit.ts:115–163)
The entire action body is wrapped in:
try {
// ... all logic ...
} catch {
// Must never throw - this is a hook command.
}There is no code path in the action that exits non-zero. The smoke test code even comments: "Never relies on subprocess exit code — audit record --from-hook never throws by design." Yet Claude Code reports a non-zero exit.
2. Hook is duplicated across two settings files
swamp init writes the hook to .claude/settings.local.json. In this repo the hook also exists in .claude/settings.json (the git-tracked project settings). Claude Code loads both files and fires all matching hooks — so swamp audit record --from-hook fires twice per Bash call, racing on the same stdin pipe. The agent-config-loadable doctor check only validates settings.local.json and would not detect this duplication.
3. swamp help audit --json reports subcommands: []
The JSON schema for the audit command lists no subcommands, even though swamp audit record --from-hook is functional. This may be a documentation gap rather than a functional bug, but it's worth noting.
Hypothesis
The most likely cause is either:
- A Cliffy framework-level error that escapes the try/catch (e.g. option parsing issue triggered only by Claude Code's invocation environment)
- A hook timeout: Claude Code kills the hook process after a deadline, causing a non-zero exit that the try/catch cannot prevent
- A race condition from the duplicate hook entries causing two concurrent reads from the same stdin pipe
Workaround
Remove the duplicate hook from .claude/settings.json so it only exists in .claude/settings.local.json (where swamp init writes it). This halves the hook invocations per Bash call.
Shipped
Click a lifecycle step above to view its details.
dmc commented 7/1/2026, 1:03:34 AM
The agent-config-loadable check currently only reads .claude/settings.local.json. It should also scan .claude/settings.json (the git-tracked project settings file) and fail — or at least warn — when the same swamp audit record --from-hook hook appears in both files. Duplicate entries cause the hook to fire twice per Bash call and may be the trigger for the non-zero exit being reported.
dmc commented 7/1/2026, 1:06:45 AM
Related question on hook placement and the global settings tier: Claude Code has three settings levels — ~/.claude/settings.json (global, all projects), .claude/settings.json (project, git-tracked), and .claude/settings.local.json (project-local, gitignored). swamp init currently writes the hook to .claude/settings.local.json, which is correct for per-project personal tooling. But swamp doctor audit's agent-config-loadable check only looks at .claude/settings.local.json — it would miss a hook that lives in the global ~/.claude/settings.json.\n\nHow should this work with a global setup? If a user puts the hook in ~/.claude/settings.json so audit recording fires across all their projects without needing to run swamp init in each one, the doctor would incorrectly report the hook as missing. Should swamp init support a --global flag that writes to ~/.claude/settings.json instead? And should doctor check the global file as a fallback before failing?
dmc commented 7/1/2026, 1:08:31 AM
Confirmed: removing the duplicate hook from .claude/settings.json (leaving it only in .claude/settings.local.json) resolved the error. Ran multiple Bash tool calls in Claude Code after the removal and the 'PostToolUse:Bash hook error / Failed with non-blocking status code' no longer appears. The duplicate was the root cause.
dmc commented 7/1/2026, 1:10:22 AM(edited)
Closing — root cause confirmed and resolved.
(Was trying to get claude to close the /btw and it tried to close the issue instead
dmc commented 7/1/2026, 1:10:43 AM
Reopening — the underlying doctor/init improvements (detecting duplicates, checking global settings) are still open work items.
Sign in to post a ripple.