Headless -p "/skill" with a failing !-preamble silently aborts pre-turn and reports subtype:success, num_turns:0
Summary
In headless mode, when the prompt is a slash-command skill invocation (claude -p "/my-skill") and that skill's SKILL.md contains a ` !command ` dynamic-injection preamble whose command fails (non-zero exit, stderr-only output), Claude Code silently aborts the session before the first model turn — and reports it as a success:
{"type":"result","subtype":"success","is_error":false,"num_turns":0,"result":"","duration_api_ms":0, ...}
No error is surfaced anywhere: exit code 0, is_error:false, subtype:"success", empty result. For unattended/scripted use this is the worst failure shape — the run did nothing and the caller is told it succeeded.
Reproduction (Claude Code 2.1.233, macOS 26.5.2)
- Create a skill:
mkdir -p probe/.claude/skills/probe-test
cat > probe/.claude/skills/probe-test/SKILL.md <<'SKILL'
---
name: probe-test
description: Probe for failing preamble behavior
---
!`sh -c 'echo "probe stderr boom" >&2; exit 42'`
Report the injected output above.
SKILL
cd probe
- Run it as the immediate prompt:
claude -p "/probe-test" --max-turns 3 --output-format json
Observed: num_turns:0, result:"", subtype:"success", duration_api_ms:0 — no API call is ever made, no error reported.
Expected: either the model turn runs (with the failure text injected as context), or the abort is reported as an error (is_error:true / non-zero exit / an error subtype) — anything but silent success.
Three observations that localize it
- Only when the skill is the first token of the prompt.
claude -p "run the /probe-test skill and report the output"works fine even with the failing preamble — the session runs and the model sees the error text. - Only when the preamble fails. Guarding the same command fail-open — `
!sh -c '...; exit 42' || echo "probe command failed with exit $? - continue"— makes the immediate-invocation form work:num_turns:1`, and the model's result quotes both the stderr text and the echo (stderr and stdout of injected commands are combined and delivered — which also shows the failure text was available; it just never reached a turn in the unguarded case). - Not sandbox-related. We first hit this inside macOS sandboxed runs (the preamble command only failed there) and chased the sandbox for days; the repro above is completely unsandboxed. The
success/num_turns:0shape made the abort invisible and misattributed.
It looks like a fail-fast on preamble failure that (a) applies only on the immediate-slash-invocation path, and (b) maps the abort to a success result.
Ask
The abort-on-failing-preamble behavior may well be intentional; the report shape is the bug. A zero-turn abort should be distinguishable from a successful run in --output-format json (and ideally in the exit code).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗