[BUG] Headless: denied skill-frontmatter shell substitution reported as `success` with num_turns:0 — no typed error, only <local-command-stderr>

Status Open
Reported on v2.1.217
Maintainer reply None cached
Activity 2 comments · opened Jul 22, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

In headless/print mode (claude -p, and the Agent SDK), when a slash command's SKILL.md frontmatter contains a shell substitution (` ! `) that isn't pre-authorized, the pre-loop expansion is denied, the prompt is consumed, and the model never runs — but the result envelope reports success:

{ "subtype": "success", "is_error": false, "num_turns": 0, "result": "", "permission_denials": [], "errors": null }

The only signal of the failure is an untyped string inside a synthetic user message:

<local-command-stderr>Error: Shell command permission check failed for pattern "!`sh ./hello.sh`": This command requires approval</local-command-stderr>

Every typed channel the SDK declares for exactly this case is left empty:

  • permission_denials: SDKPermissionDenial[][]. Its own doc string in sdk.d.ts says the type exists to surface "headless-agent auto-deny … so SDK hosts can render the denial."
  • SDKResultError carries subtype: 'error_during_execution' | … and errors — unused; the envelope is SDKResultSuccess.

So the same event is reported as success in the typed fields and as an error in the untyped side-channel. For any programmatic (headless/SDK) consumer this is a silent no-op: no output, no error, nothing to key on except string-matching a terminal-rendering artifact.

I'm not reporting the denial itself — routing frontmatter substitution through canUseTool would mean installing a skill implies arbitrary shell execution via any permissive callback, so hard-denying it pre-loop is a defensible security choice. The reporting is the bug: this path appears to be written for an interactive terminal (a human to approve, stderr on screen), and both assumptions are absent under -p/SDK.

What Should Happen?

A prompt that was consumed but never ran should not be reported as success. Any of these fixes the programmatic contract:

  • is_error: true with a subtype naming the denial (e.g. error_during_execution), and/or
  • a populated permission_denials[] entry for the blocked substitution (the field already exists for this), and/or
  • the denial surfaced in errors.

At minimum, the typed result and the <local-command-stderr> side-channel should not contradict each other.

Error Messages/Logs

The full stream (--output-format stream-json --verbose) contains only: system (init) → one synthetic user message (the <local-command-stderr> line above) → result (the success envelope above). There is no assistant message — the model never ran.

Steps to Reproduce

mkdir -p repro/.claude/skills/single && cd repro
printf 'echo MARKER_OK\n' > hello.sh
cat > .claude/skills/single/SKILL.md <<'SKILL'
---
name: single
description: probe
---
Injected: !`sh ./hello.sh`
SKILL

claude -p "/single" --setting-sources=project --permission-mode default \
  --output-format stream-json --verbose

Observed: subtype:"success", is_error:false, num_turns:0, permission_denials:[], empty result.
Expected: a result that identifies the permission denial in a typed field.

Confirming it is specifically the denial (both make it expand and run, num_turns ≥ 1, with MARKER_OK substituted into the prompt):

  • pass --allowedTools "Bash(sh:*)", or
  • add a Bash(sh:*) allow rule to .claude/settings.json.

Claude Model

N/A — the model never runs (num_turns: 0).

Is this a regression?

Not sure (no known-good prior version identified).

Last Working Version

_(unknown)_

Claude Code Version

2.1.217

Platform

Claude Code CLI (headless -p) / Agent SDK

Operating System

Linux

Terminal/Shell

bash

Additional Information

  • Reproduced on 2.1.217 (and 2.1.191). SDK type references: @anthropic-ai/claude-agent-sdk/sdk.d.tsSDKResultSuccess / SDKResultError (num_turns, permission_denials, errors) and the SDKPermissionDenial doc noting headless-agent auto-deny.
  • Possibly related false-success-in-headless case: #74761 — but there the model did run and the loop exited mid-task; here the model never runs (num_turns: 0), so it's a distinct trigger. Docs gap on error payloads: #41265.
  • There is a large cluster of downstream reports of the denial firing on skill frontmatter; the silent-success reporting is what makes those hard to diagnose in headless/SDK setups.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗