feat: --prompt-file flag to read long prompts from disk (v1.2.4)#13
Merged
JonyanDunh merged 1 commit intomainfrom Apr 11, 2026
Merged
feat: --prompt-file flag to read long prompts from disk (v1.2.4)#13JonyanDunh merged 1 commit intomainfrom
JonyanDunh merged 1 commit intomainfrom
Conversation
Add a new `--prompt-file <path>` argument to `/watchdog:start` so users
can pass multi-paragraph prompts (Markdown task specs with headings,
code fences, quoted strings, etc.) without mangling them through shell-
argument escaping.
## Problem
Slash command `!` shell blocks invoke setup-watchdog.js via `$ARGUMENTS`
literal substitution. Any unescaped `"`, backtick, `$`, or newline in
the user's inline prompt breaks bash parsing with `unexpected EOF`. In
practice this hits every realistic multi-paragraph Markdown prompt the
moment a user pastes it into `/watchdog:start "..."`.
ralph-loop (from which watchdog is derived) has the same failure mode
and does not provide a file or stdin fallback.
## Solution
New `--prompt-file <path>` flag in scripts/setup-watchdog.js. The file
is read directly with Node's `fs.readFileSync`, bypassing shell
argument parsing entirely. Mutually exclusive with an inline `<PROMPT>`
positional — pick one or the other.
Path handling is delegated to `path.resolve()`, which is platform-
aware:
- Linux/macOS/WSL POSIX paths (absolute + relative, `./name`, `../name`)
- Windows `C:\...`, `C:/...`, and UNC `\\server\share\...` paths
- Relative paths resolve against `process.cwd()` on every platform
Leading UTF-8 BOM is stripped automatically — Windows Notepad and
PowerShell's default `Set-Content` silently prepend U+FEFF to UTF-8
files, and `.trim()` does not remove it (BOM is not whitespace), so
without this Claude would see an invisible zero-width marker as the
first character of the prompt.
CRLF line endings inside the content are preserved byte-for-byte. `~`
is NOT expanded by watchdog — that is the shell's job, and bash/zsh
already expand it before the args reach the script. `cmd.exe` users
should pass an absolute path or `%USERPROFILE%\...`.
Error paths return clean messages:
- ENOENT => "prompt file not found: <resolved>"
- EISDIR => "--prompt-file expects a file, got a directory: <resolved>"
- EACCES/EPERM => "permission denied reading prompt file: <resolved>"
- empty file => "prompt file is empty: <resolved>"
- missing arg => "--prompt-file requires a path argument"
- combined with inline => "--prompt-file cannot be combined with a
positional prompt"
## What changed
- scripts/setup-watchdog.js — parseArgs() recognizes --prompt-file,
new readPromptFile() helper centralizes file read + BOM strip +
error mapping, main() enforces mutual exclusion
- commands/start.md — argument-hint updated to show the alternate
form
- commands/help.md — new --prompt-file option with the full path-
handling reference (POSIX / Windows / UNC / BOM / CRLF / spaces /
encoding / ~ expansion)
- test/setup.test.js — +17 tests: happy path, shell metacharacters
in content, whitespace trimming, UTF-8 BOM strip, relative path,
./name relative path, CRLF content, non-ASCII filename, symlink
following (non-Windows), permission denied (non-Windows non-root),
missing file, directory target, empty file, missing path arg,
mutual exclusion with inline, help text listing new form
- README.{md,zh,es,ja,ko,vi,pt}.md — new "Long prompts from a file"
subsection under Commands, plus a new "Prompt input" row in the
Watchdog vs ralph-loop comparison table
- .claude-plugin/plugin.json — version => 1.2.4
- .claude-plugin/marketplace.json — version => 1.2.4 (both the
plugin entry and the marketplace top-level)
## Test plan
- [x] 96-test suite passes locally (94 active + 2 skipped-inside-
Claude-Code, 0 failures)
- [x] setup.test.js: 27 tests — 26 active + 1 pre-existing skip
- [x] Root cause debugged by reproducing the real bash `unexpected
EOF` on a pasted multi-paragraph Markdown prompt
- [ ] CI will confirm on the standard matrix (ubuntu/macos/windows
× Node 18/20/22)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a new
--prompt-file <path>argument to/watchdog:startso users can pass multi-paragraph prompts (Markdown task specs with headings, code fences, quoted strings, etc.) without mangling them through shell-argument escaping.Problem
Slash command
!shell blocks invokesetup-watchdog.jsvia$ARGUMENTSliteral substitution. Any unescaped", backtick,$, or newline in the user's inline prompt breaks bash parsing withunexpected EOF. In practice this hits every realistic multi-paragraph Markdown prompt the moment a user pastes it into/watchdog:start "...".ralph-loop (from which watchdog is derived) has the same failure mode — I confirmed this against the upstream
setup-ralph-loop.shsource — and does not provide a file or stdin fallback.Solution
New
--prompt-file <path>flag inscripts/setup-watchdog.js. The file is read directly with Node'sfs.readFileSync, bypassing shell argument parsing entirely. Mutually exclusive with an inline<PROMPT>positional — pick one or the other.Path handling
Delegated to
path.resolve(), which is platform-aware:/home/you/...) and relative (./tmp/...,../notes.txt) pathscmd.exe/ PowerShell) —C:\Users\you\...,C:/Users/you/..., and UNC\\server\share\...paths all work through Node'sfsAPIsprocess.cwd()on every platformBOM / CRLF / encoding
Set-Contentsilently prepend U+FEFF to UTF-8 files, and.trim()does not remove it (BOM is not whitespace), so without this Claude would see an invisible zero-width marker as the first character of the prompt~is NOT expanded by watchdog — that is the shell's job. bash/zsh already expand it before the args reach the script;cmd.exeusers should pass an absolute path or%USERPROFILE%\...Error paths
All return exit 1 with a clear
stderrmessage:ENOENTprompt file not found: <resolved>EISDIR--prompt-file expects a file, got a directory: <resolved>EACCES/EPERMpermission denied reading prompt file: <resolved>prompt file is empty: <resolved>--prompt-file requires a path argument--prompt-file cannot be combined with a positional promptWatchdog vs ralph-loop — new comparison row
Added a Prompt input row to the comparison table in all 7 READMEs, reflecting that watchdog now has a clean escape-hatch that ralph-loop lacks:
$ARGUMENTS, or--prompt-file <path>— reads the file directly with Node'sfs.readFileSync, bypassing shell argument parsing entirely. Safe for multi-paragraph Markdown containing newlines, quotes, backticks,$, etc. UTF-8 BOM is stripped automatically; CRLF is preserved byte-for-byte.$ARGUMENTSin the slash command's!shell block only. Any unescaped", backtick,$, or newline in the prompt breaksbashparsing withunexpected EOF. No file or stdin fallback.Verified against the upstream source at https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-loop —
setup-ralph-loop.shonly parses--max-iterations,--completion-promise,-h/--help, and positional args.Scope
scripts/setup-watchdog.js—parseArgs()recognizes--prompt-file, newreadPromptFile()helper centralizes file read + BOM strip + error mapping,main()enforces mutual exclusioncommands/start.md—argument-hintupdated to show the alternate formcommands/help.md— new--prompt-fileoption with the full path-handling referencetest/setup.test.js— +17 tests: happy path, shell metacharacters in content, whitespace trimming, UTF-8 BOM strip, relative path,./namerelative path, CRLF content, non-ASCII filename, symlink following (non-Windows), permission denied (non-Windows non-root), missing file, directory target, empty file, missing path arg, mutual exclusion with inline, help text listing new formREADME.{md,zh,es,ja,ko,vi,pt}.md— new "Long prompts from a file" subsection under Commands, plus the new "Prompt input" row in the Watchdog vs ralph-loop comparison table.claude-plugin/plugin.json— version → 1.2.4.claude-plugin/marketplace.json— version → 1.2.4 (both the plugin entry and the marketplace top-level)Zero changes to
lib/orhooks/. Purely additive: no existing behavior of/watchdog:startis modified — users who already pass inline prompts see no change.Test plan
setup.test.jsalone: 27 tests — 26 active + 1 pre-existing skipunexpected EOFon a pasted multi-paragraph Markdown promptInstall / upgrade
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com