[BUG] `claude -p` / `claude --print` runs are classified as `sdk-cli` and diverge from TUI request scaffold

Resolved 💬 3 comments Opened May 14, 2026 by oxysoft Closed May 18, 2026

Context note: This is a frontend/product-boundary parity issue, not a criticism
of any individual implementation. Since -p / --print behavior is already a
known pain point, and upcoming mid-June frontend-facing changes may depend on
predictable CLI/TUI parity, this should be evaluated as a user-facing parity
risk. The goal is to reduce verification time and avoid customer-facing
regressions with potential business impact.

Summary

In Claude Code 2.1.141, claude -p and other non-TTY CLI invocations are not
sent as non-interactive Claude Code CLI sessions. They are classified as
sdk-cli, which changes the API-visible request scaffold compared with the
interactive TUI path.

This is subtle because the final user prompt block can remain clean. The
problem is the surrounding scaffold: product identity, request attribution, beta
flags, tool contracts, and default system prompt text differ for the same prompt.

Why this matters

-p is used in frontend-facing workflows, CI, evaluation loops, scripts,
scheduled jobs, and internal developer tooling. Users reasonably expect it to
be a machine-readable output mode for Claude Code. If the TUI and -p send
different request scaffolds:

  • incidents reproduced manually in TUI are not reproducing the printed run
  • evals using -p may not measure TUI Claude Code behavior
  • hooks, skills, MCP, permissions, auth, and tool behavior can drift between

manual and printed workflows

  • support teams receive conflicting reports from interactive and print-mode use
  • users cannot audit the difference from stdout alone

LLM context is product behavior. Product identity and tool contracts are not
cosmetic metadata; they influence model behavior and user trust.

Version

  • Claude Code wrapper package: @anthropic-ai/claude-code@2.1.141
  • Native package: @anthropic-ai/claude-code-linux-x64@2.1.141
  • Native build time in bundle: 2026-05-13T21:26:59Z
  • Native git SHA in bundle: 4f4623ddd339e1c1b87d659b7c9eb3b66397e7a3

Reproduction

Run Claude Code against a local recorder that captures /v1/messages and
returns an intentional 400. This avoids contacting the real API.

scripts/capture-anthropic.py --port 18082 --out tmp/capture/print.jsonl

Then:

env \
  ANTHROPIC_API_KEY=sk-ant-capture \
  ANTHROPIC_BASE_URL=http://127.0.0.1:18082 \
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  CLAUDE_CODE_DISABLE_AUTOUPDATER=1 \
  claude -p "TRACE_PROMPT_ALPHA" \
    --model claude-sonnet-4-5 \
    --output-format json \
    --debug-file tmp/capture/print-debug.log

Run the TUI against the same recorder and submit TRACE_PROMPT_ALPHA.
Ignore the TUI's first generate_session_title request; compare -p against
the TUI repl_main_thread request.

Rediscovery track

This can be rechecked across minified releases without relying on local symbol
names:

  1. Capture a claude -p / claude --print request and a TUI main-thread

request from the same repo, config, model, and prompt state.

  1. Select comparable packets by debug source: sdk for print mode,

repl_main_thread for the TUI main request.

  1. Compare User-Agent, the first attribution system block, the identity

system block, anthropic-beta, tools[].name, top-level body keys, and
the final user content block.

  1. If source inspection is needed, search stable literals:

CLAUDE_CODE_ENTRYPOINT, sdk-cli, --print, process.stdout.isTTY,
User-Agent, and the Claude Code / Agent SDK identity prompt strings.

Observed packet deltas

| Field | claude -p | TUI main-thread request |
| --- | --- | --- |
| debug source | source=sdk | source=repl_main_thread |
| User-Agent | claude-cli/2.1.141 (external, sdk-cli) | claude-cli/2.1.141 (external, cli) |
| attribution system block | cc_entrypoint=sdk-cli | cc_entrypoint=cli |
| identity system block | You are a Claude agent, built on Anthropic's Claude Agent SDK. | You are Claude Code, Anthropic's official CLI for Claude. |
| beta header | lacks redact-thinking-2026-02-12 | includes redact-thinking-2026-02-12 |
| tools | 23 tools, including Glob, Grep, TodoWrite | 24 tools, including TaskCreate, TaskGet, TaskList, TaskUpdate |

Matched fields in the simple capture:

  • same model
  • same thinking
  • same context_management
  • same user content block order after matching CLAUDE.md include approval state
  • final user prompt block is clean

So this is not primarily a bad token in user content. It is request scaffold
misclassification.

Matrix coverage

The same sdk-cli scaffold appears across:

  • claude -p "prompt" --output-format text
  • claude -p "prompt" --output-format json
  • claude -p "prompt" --output-format stream-json --verbose
  • no -p but stdout not a TTY
  • stdin-only prompt
  • positional prompt plus stdin
  • --input-format stream-json
  • replayed stream JSON input
  • --append-system-prompt
  • --system-prompt
  • --exclude-dynamic-system-prompt-sections
  • --tools ""
  • --json-schema
  • CLAUDE_CODE_ENTRYPOINT=cli claude -p ...

Every covered row still sent source=sdk / sdk-cli.

Suspected cause

In the minified linux-x64 bundle:

  • startup computes one non-interactive boolean from -p, --init-only,

--sdk-url, and !process.stdout.isTTY

  • the entrypoint setter maps that boolean to sdk-cli or cli
  • the identity prompt selector keys SDK identity off isNonInteractive
  • User-Agent and request attribution serialize CLAUDE_CODE_ENTRYPOINT

This collapses two separate concepts:

  • headless CLI output mode
  • Agent SDK entrypoint / SDK transport

Expected behavior

Plain claude -p "prompt" should remain a Claude Code CLI session with
non-interactive output. It should preserve CLI identity and request scaffold
unless the caller is actually using an SDK entrypoint.

Suggested fix

Split the current classifier into separate concepts:

  • isNonInteractive: controls TUI suppression, output formatting, setup prompt

skipping, and other UI concerns

  • isSdkEntrypoint: controls CLAUDE_CODE_ENTRYPOINT=sdk-cli, SDK identity,

SDK request attribution, and SDK-specific tool/agent surfaces

Then make -p and stdout piping set isNonInteractive=true without implying
isSdkEntrypoint=true.

Related issues

Filed split for focused tracking:

This may explain why many -p/headless issues do not reproduce in TUI:

  • #18131 - API 400 tool-use concurrency issue only in print mode
  • #20508 - duplicate tool_use IDs in -p
  • #33343 - hooks and --allowedTools not enforced in headless -p
  • #36570 - skills do not trigger in claude -p
  • #44756 - skills silently fail in --print / -p combinations
  • #32184 - skill triggering recall is 0% in headless mode
  • #39453 - -p cannot use existing Claude Pro login
  • #56540 - non-TTY -p hangs with parallel Task fan-out
  • #52917 - -p exits before background subagents complete
  • #55802 - startup launches many headless sdk-cli sessions

Mitigation while upstream is fixed

claude-p demonstrates a safer workaround class:

It drives the real interactive Claude Code TUI through a PTY, injects the prompt
via hooks, reads the transcript after Stop, and emits claude -p-compatible
output. That preserves the TUI scaffold by construction and avoids rewriting API
packets.

View original on GitHub ↗

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