Workflow({ name: "..." }) fails schema validation ("control characters that would be hidden in the approval dialog") — identical script works fine via inline `script`
Title
Workflow({ name: "..." }) fails schema validation ("control characters that would be hidden in the approval dialog") — identical script works fine via inline script
Summary
Invoking the Workflow tool by name against a project-local named workflow (.claude/workflows/review-adversarial.js, registered via a matching skill) fails on every attempt with:
The permission handler returned updatedInput for Workflow that failed schema validation: [
{
"code": "custom",
"path": ["script"],
"message": "script contains control characters that would be hidden in the approval dialog"
}
]
This is a configuration issue in your canUseTool callback, PermissionRequest hook, or permission-prompt tool — updatedInput must satisfy the tool's input schema. The tool input from the model was valid.
Passing the exact same script content inline via the script parameter (rather than name) succeeds immediately with no error, launches in the background, and runs the real multi-agent pipeline correctly (agent(), pipeline(), parallel() all execute as expected).
Environment
- Windows 11, Claude Code CLI, working directory a Windows filesystem path (
C:\Developement\...), Git Bash as the shell. - Repo: private, not shareable — but the workflow script itself is boilerplate JS with no unusual content (see diagnostics below).
Reproduction steps
- Have a named workflow registered under
.claude/workflows/<name>.js, invokable viaWorkflow({ name: "<name>", args: ... })(in our case, exposed through a slash-command skill that itself callsWorkflow({ name: "review-adversarial", args: "..." })). - Call
Workflow({ name: "review-adversarial", args: "<any string>" }). - Observe the schema-validation error above, every time, regardless of
argscontent. - Copy the exact literal content of
.claude/workflows/review-adversarial.jsinto thescriptparameter of a freshWorkflowcall (samemeta, same body, byte-for-byte), with an equivalentargsvalue. - Observe it succeeds immediately — no error, launches in the background, runs correctly.
What we ruled out before filing this
Before concluding this was a genuine tool/harness bug rather than something wrong with our file, we tested three specific hypotheses directly rather than assuming:
- CRLF line endings. The on-disk file (Windows checkout,
core.autocrlf=true) had CRLF endings while the committed git blob was LF-only. We stripped every\rfrom the local working copy and retriedWorkflow({ name: ... })— same identical error. Ruled out. - Embedded ASCII control characters or a BOM. Byte-level scan (
grep -naP '[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]') found zero matches anywhere in the file. No BOM at the start of the file (xxdconfirms it starts directly with// Executor:). Ruled out. - Hidden Unicode formatting characters (zero-width space/joiner, bidi override characters,
U+FEFF). Scanned for the common ranges (U+200B–U+200F,U+202A–U+202E,U+2060–U+2069,U+FEFF,U+00AD) — zero matches. Ruled out. - Whether the
Workflowtool itself is broken generally. Ran a trivial 4-line inline script viascript(notname) — succeeded instantly. Ruled out — the tool's execution path is fine. - Whether it's the specific script content that's the problem, independent of name-vs-inline. Ran the exact same real script (the one that fails under
name) via inlinescriptinstead — succeeded instantly, ran the full real pipeline correctly against a real PR diff.
Conclusion
The bug is isolated to whatever the harness does when resolving a script by name before handing it to the permission-approval dialog — not the script's content, and not the Workflow tool's execution engine. Our best guess (unconfirmed, no visibility into the resolution internals) is that the by-name resolution path wraps or re-serializes the script with something — a metadata banner, a path string with backslashes, a preview-formatting layer — that itself introduces the offending characters (plausibly literal ANSI escape codes, which would fit "control characters that would be hidden in the approval dialog" almost exactly). This could also be Windows-path-specific (backslash-heavy paths going through a step that assumes POSIX paths, though we didn't get to test on a different OS to confirm).
Workaround in use
Until this is fixed, we're invoking review-adversarial (and presumably any other named workflow that hits this) by pasting its exact file content into the script parameter instead of using name. This works but obviously isn't sustainable as a long-term pattern for every repo adopting workflow-based tooling.
Impact
This blocks the intended UX for any named/reusable Workflow — the whole point of name + a registered .claude/workflows/*.js file is that callers (including slash-command skills) shouldn't need to carry the full script inline every time. Right now that shortcut is completely non-functional.