Skip to main content
← Back to list
01Issue
BugShippedSwamp CLI
Assigneesstack72

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 output

The 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

  1. Run swamp init --tool claude to wire the PostToolUse hook in .claude/settings.local.json
  2. Use Claude Code and run any Bash tool call
  3. 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 output

swamp 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.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 4 MOREPR_MERGED+ 1 MORECONTRIBUTOR_NOTIFIED

Shipped

7/1/2026, 6:13:19 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack727/1/2026, 3:41:19 PM
Editable. Press Enter to edit.

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.

stack72 commented 7/1/2026, 6:13:31 PM

Thanks @dmc for reporting this! The fix has been merged and a release is on its way. We appreciate your contribution to swamp.

Sign in to post a ripple.