[FEATURE] Workflow tool: byte-exact data channel between workflow scripts and the host (model-retyped transport corrupts commands and payloads)

Status Open
Reported on v2.1.172
Maintainer reply None cached
Activity 5 comments · opened Jun 11, 2026

Summary

Workflow scripts (the Workflow tool, CLAUDE_CODE_WORKFLOWS=1) have no byte-exact way to move text between the script and the host. The sandbox bans filesystem, process, and clock access for replay determinism, so the only effector a script has is agent(). Every shell command a script wants to run must travel inside a subagent's prompt, and the subagent re-types it into its Bash tool call. Every result travels back as the subagent's final text. A language model therefore sits in the middle of every byte that crosses the script boundary, in both directions, and a model is not a reliable copier of long or quote-bearing strings.

We run a long-lived multi-agent orchestrator as a Workflow script (60+ subagent spawns per run, durable state checkpointed between runs). Over five consecutive runs, five distinct production failures traced to this single restriction. We believe the failure class is structural and worth a first-class fix in the Workflow API.

Observed failure modes (all reproduced in production runs)

  1. Multi-KB argument mis-escaped into invalid shell. A ~4 KB JSON document was passed as one argv element in the runner subagent's prompt ("pass each element as one argument verbatim"). The Bash-only subagent composed a double-quoted shell string, hand-escaped the inner quotes, and bash failed with exit 2 (unexpected EOF while looking for matching quote) before the target program ran. The workflow crashed.
  1. Silent single-token corruption on a write that exited 0. A ~100-entry key list crossed the prompt boundary with exactly one key altered. The command succeeded, the corrupted value persisted into durable state, and a later resume crashed on it. Nothing at write time could detect this without an application-level checksum.
  1. Mixed escape interpretation across subagent instances. The same prompt rendering (a JSON.stringify'd shell line) was decoded by one subagent instance (typed plain quotes) and copied verbatim by another (typed the escaped bytes \" literally). The same prompt produced different bytes on the wire depending on which sample of the model handled it.
  1. Behavioral deviation instead of copy error. A subagent ran the requested command correctly, then ran an additional command of its own and returned that second command's output as the requested command's stdout. A retry re-rolled the identical deviation 3 out of 3 times — retrying an LLM-backed step is not independent sampling when the prompt context is identical.
  1. Error text loss in the harness itself. When a Workflow script dies on an uncaught exception, the failed-run record serializes it as a bare Error with no message, so the crash cause has to be reconstructed from other evidence. This is the same theme at the harness layer: text crossing a process boundary loses fidelity.

Workarounds we had to build

Each of these works, and each is machinery no application author should have to write:

  • A chunked base64 stdin courier (tee with exact-byte input) for every file write, because inline content cannot survive the prompt hop.
  • FNV checksums embedded in durable state, verified with bounded re-reads on every read, because reads come back model-typed.
  • Redirect-to-file plus a separate cat re-read for command outputs, because inline stdout can be transcribed wrong.
  • Composition-time guards that reject double quotes, backslashes, and non-ASCII bytes from any composed command line, because some bytes reliably break the copy.
  • A rule that any payload over roughly 1 KB must ride a file, never a prompt.

The five failures above each cost a full diagnose / fix / review / relaunch cycle. The checksum, courier, and guard code now make up a meaningful fraction of our orchestrator.

Feature request

A deterministic, byte-exact channel between a Workflow script and the host. Any one of these would eliminate the class; they are listed in order of how completely they solve it:

  1. A harness-executed exec(argv, {input}) primitive in the Workflow API. The harness spawns the process directly (no model in the path) and journals the result exactly the way agent() results are journaled today, so resume/replay determinism is fully preserved.
  1. Sandbox-scoped byte-exact file primitives (readFile/writeFile under a per-run directory), journaled the same way.
  1. Verbatim tool-call binding for a subagent's first action: something like agent(prompt, {initialToolCall: {name: 'Bash', input: {...}}}), where the harness executes the given tool call with the given bytes and the model only interprets the result. The model adds judgment where judgment is wanted and is removed from the copy path where it is not.
  1. Artifact passing on agent(): accept and return file references rather than inline text, so large payloads never enter a prompt.

The sandbox restrictions exist for replay determinism, and we are not asking to weaken them. agent() already proves the pattern: a non-deterministic effect whose result is journaled is replay-safe. A process spawn is far more deterministic than a model call, so the same journaling mechanism covers it.

Environment

  • Claude Code 2.1.172
  • Windows 11 (host), subagent shell is Git Bash (POSIX)
  • Workflow tool enabled via CLAUDE_CODE_WORKFLOWS=1
  • Orchestrator pattern: one Workflow script, Bash-only haiku command-runner subagents, sonnet/opus worker subagents

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/63102
  2. https://github.com/anthropics/claude-code/issues/60325
  3. https://github.com/anthropics/claude-code/issues/66745

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

abhinas90 · 2 months ago

This is a trust/safety event, not just a bug. What I recommend in these cases:

  1. Immediate post-mortem: check \~/.claude/projects/\ for the session that produced the delete. The JSONL session logs will show the exact tool calls and model reasoning.
  1. Filesystem audit trail: if the project is under git, \git reflog\ + \git fsck --lost-found\ can recover dangling blobs even after a destructive commit.
  1. Permission hardening: run Claude Code with a restricted user that lacks write access outside the project root. A \chroot\ or Docker container wrapper takes 5 minutes to set up and prevents this class of failure entirely.
  1. Pre-flight hook: a simple pre-exec hook that snapshots \git status --porcelain\ before every Claude Code session gives you a rollback point.

I've got a safety hardening checklist if you want it — this pattern is more common than you'd think.

ghbaud · 2 months ago

Not a duplicate — keeping this open. On the three flagged candidates:

  • #60325 (find/grep shadow functions dispatching a nested agent) is unrelated to this report.
  • #63102 (resume cache unreachable because an LLM dispatcher cannot transcribe args byte-exactly) and #66745 (write agents author a status report instead of the provided content; schema-read agents return null) are adjacent instances of the same root cause this issue names: inside the Workflow feature, a model sits in the copy path for bytes that need to be moved verbatim, and a model is not a reliable byte copier. #63102 hits it on the args-transcription hop, #66745 hits it on the agent-write hop, and this issue documents five more production failures on the script-to-subagent command hop.

This issue is the feature request for the missing primitive — a harness-executed, journal-replayable byte-exact channel (exec, sandbox file primitives, verbatim tool-call binding, or artifact passing) — that would eliminate the class all three reports belong to, rather than a report of one more instance. Cross-linking them so triage can see the pattern.

zwrose · 1 month ago

Independent corroboration from a second production orchestrator — an open-source Claude Code plugin (zwrose/superheroes) whose pipeline runs a multi-phase spec→PR workflow as a Workflow-tool script. Because the script's only effector is agent(), every file read, file write, and CLI call in a run is a full agent dispatch whose stdout round-trips through a model. We've independently built the same defenses this issue describes: a dedicated minimal courier agent (~2.6× cheaper per leaf), content-hash CAS writes with read-back verification, staging any large payload to files instead of inlining it, and a corrective-retry protocol for mangled answers. Observed live failure modes before those defenses: re-typed multi-KB JSON records mangled in transit, silent field drops in terminal records, fence-wrapped answers breaking parsers, and a courier answering with a probe command's stdout instead of the real command's.

One data point that supports this issue's proposed design: we refactored so that content stays on disk and only O(1) semantic JSON (gates, verdicts, scalars) crosses the script boundary — and transport failures stopped entirely. The boundary is survivable when payloads are tiny; the remaining cost is that every deterministic side effect still burns an agent dispatch (tokens + latency + a retry apparatus). A journaled exec(argv, {input}) as proposed here would let the sandbox keep its replay guarantees while removing the model from the copy path — for our workload it would delete an entire defensive layer (couriers, fence-tolerant parsing, read-back verification) that exists only because a model is not a wire.

Showing cached comments. Read the full discussion on GitHub ↗