Workflow({ name: "..." }) fails schema validation ("control characters that would be hidden in the approval dialog") — identical script works fine via inline `script`

Open 💬 0 comments Opened Jul 7, 2026 by CharlesBerg85

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

  1. Have a named workflow registered under .claude/workflows/<name>.js, invokable via Workflow({ name: "<name>", args: ... }) (in our case, exposed through a slash-command skill that itself calls Workflow({ name: "review-adversarial", args: "..." })).
  2. Call Workflow({ name: "review-adversarial", args: "<any string>" }).
  3. Observe the schema-validation error above, every time, regardless of args content.
  4. Copy the exact literal content of .claude/workflows/review-adversarial.js into the script parameter of a fresh Workflow call (same meta, same body, byte-for-byte), with an equivalent args value.
  5. 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:

  1. 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 \r from the local working copy and retried Workflow({ name: ... })same identical error. Ruled out.
  2. 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 (xxd confirms it starts directly with // Executor:). Ruled out.
  3. Hidden Unicode formatting characters (zero-width space/joiner, bidi override characters, U+FEFF). Scanned for the common ranges (U+200BU+200F, U+202AU+202E, U+2060U+2069, U+FEFF, U+00AD) — zero matches. Ruled out.
  4. Whether the Workflow tool itself is broken generally. Ran a trivial 4-line inline script via script (not name) — succeeded instantly. Ruled out — the tool's execution path is fine.
  5. 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 inline script instead — 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.

View original on GitHub ↗