Skip to content

Commit 618c3bc

Browse files
committed
docs: add technical documentation for PowerShell/CMD system prompt solutions
Detailed explanations of how the Windows shell solutions work: - PowerShell stop-parsing token and escaping mechanics - CMD command-line parsing and Git Bash invocation - Complete micro-timeline of execution for both shells These documents explain the intricate shell escaping required to pass multi-line system prompts through different Windows shells.
1 parent 9a74e53 commit 618c3bc

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
Here is a precise, end-to-end explanation of why the **CMD** one-liner works and what each part does:
2+
3+
```cmd
4+
"C:\Program Files\Git\bin\bash.exe" -lc "p=$(tr -d '\r' < ~/.claude/prompts/python-developer.md); exec claude --append-system-prompt=\"$p\""
5+
```
6+
7+
# 1) What CMD contributes
8+
9+
* **`"C:\Program Files\Git\bin\bash.exe"`**
10+
In `cmd.exe`, double quotes group a path with spaces into a single token, so the program launched is Git Bash. Inside a quoted argument for a Windows program, backslash-quote `\"` is the standard way to embed a literal double quote in the same argument, per the Microsoft C runtime command-line parsing rules that most Windows programs use. ([Microsoft Learn][1])
11+
12+
* **Everything after that becomes arguments to `bash.exe`**
13+
`-l` asks Bash to run as a login shell, and `-c "…"` tells Bash to execute the given command string. CMD passes that entire string as one argument because it is wrapped in double quotes. Also note that characters like `&` and `|` are special in CMD, which is why wrapping the `-c` payload in quotes is required to avoid CMD’s own metacharacter parsing. ([Microsoft Learn][2])
14+
15+
* **Command-line length caveat**
16+
CMD itself imposes about **8191** characters as the maximum command-line length. Your line stays far under that, so it executes reliably, but it is a useful limit to remember when you embed long data. The underlying Win32 `CreateProcess` API allows up to **32767** characters, however when you invoke via CMD you are bound by CMD’s lower limit. ([Microsoft Learn][3], [Microsoft for Developers][4])
17+
18+
# 2) What Bash does with `-lc "…"`
19+
20+
The string given to `-c` is parsed by Bash, not by CMD. Inside that string:
21+
22+
* **`~/.claude/prompts/python-developer.md`**
23+
Tilde expansion happens in Bash, `~` becomes the current user’s home. In Git Bash, home maps to your Windows profile directory and POSIX-style drive prefixing is used, for example `/c/Users/<name>`. ([GNU][5], [MSYS2][6])
24+
25+
* **`< file` input redirection**
26+
The `<` operator redirects the named file to the standard input of the command on its left. Here it feeds the prompt file into `tr`. ([GNU][7])
27+
28+
* **`tr -d '\r'`**
29+
`tr` is run from GNU coreutils. With `-d`, `tr` deletes every occurrence of the characters listed, so `'\r'` strips Windows carriage returns, leaving clean LF line endings. That prevents odd parsing issues when multi-line text later becomes a single shell argument. ([GNU][8])
30+
31+
* **`p=$( … )` command substitution**
32+
`$( … )` runs the command and substitutes its standard output. Bash captures all bytes from `tr` and assigns them to the variable `p`. Trailing newlines are removed, embedded newlines are preserved, which is exactly what you want for a multi-line system prompt. ([GNU][9])
33+
34+
* **`exec claude --append-system-prompt="$p"`**
35+
`exec` replaces the current Bash process with the `claude` CLI, so you do not keep an extra shell in the foreground. The value expansion is **double-quoted**, which is the critical part. In Bash, double quotes suppress word splitting and globbing, so the entire multi-line value in `p` is passed to `--append-system-prompt` as one argument, even if the first line begins with `---`. This avoids the classic “unknown option '---'” failure that happens when a multi-line value is accidentally split into separate argv tokens. ([GNU][10])
36+
37+
# 3) Why this pattern succeeds where others fail
38+
39+
* **CMD’s quoting and metacharacters are tricky**
40+
Without the outer double quotes, CMD would try to interpret `&`, `|`, `(`, and `)` in your Bash payload, which would corrupt the command. Quoting once at the CMD layer hands one opaque argument to `bash.exe`, and all further parsing is done by Bash, which is what you want here. ([Microsoft Learn][2])
41+
42+
* **The value is bound to the flag as a single argv element**
43+
The combination of Bash variable assignment, quoting, and `exec` ensures `claude` receives exactly two things for this feature, the flag name and one value string. Because the value is already the next argv item, the CLI parser treats it as data, not as another option, even if its first characters are `---`. The “single argument” property comes from Bash’s word-splitting rules, which say that expansions enclosed in double quotes are not split. ([GNU][11])
44+
45+
* **CRLF normalization prevents stray `\r` from leaking**
46+
CR characters in Windows-created files can sneak into an argument and confuse downstream parsers. Removing `\r` with `tr -d '\r'` is a simple, POSIX-friendly way to normalize the text before it becomes an argument. ([GNU][8])
47+
48+
# 4) Micro-timeline of execution
49+
50+
1. `cmd.exe` launches `bash.exe` with `-lc "<payload>"`. The quotes make the payload one argument, and embedded `\"` sequences survive as literal quotes inside that argument per the CRT argument rules. ([Microsoft Learn][1])
51+
2. Bash, as a login shell due to `-l`, executes the `-c` string. It expands `~`, redirects the file into `tr`, deletes `\r`, and captures the result in `p` via command substitution. ([GNU][5])
52+
3. Bash runs `exec claude --append-system-prompt="$p"`. Because `$p` is double-quoted, it is passed as one multi-line value. The shell process is replaced by the CLI, so the terminal is attached directly to `claude`. ([GNU][10])
53+
54+
That is the complete mechanics behind your working CMD line.
55+
56+
[1]: https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments?view=msvc-170&utm_source=chatgpt.com "Parsing C command-line arguments"
57+
[2]: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd?utm_source=chatgpt.com "cmd"
58+
[3]: https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation?utm_source=chatgpt.com "Command prompt line string limitation - Windows Client"
59+
[4]: https://devblogs.microsoft.com/oldnewthing/20031210-00/?p=41553&utm_source=chatgpt.com "What is the command line length limit? - The Old New Thing"
60+
[5]: https://www.gnu.org/s/bash/manual/html_node/Tilde-Expansion.html?utm_source=chatgpt.com "Tilde Expansion (Bash Reference Manual)"
61+
[6]: https://www.msys2.org/docs/filesystem-paths/?utm_source=chatgpt.com "Filesystem Paths"
62+
[7]: https://www.gnu.org/s/bash/manual/html_node/Redirections.html?utm_source=chatgpt.com "Redirections (Bash Reference Manual)"
63+
[8]: https://www.gnu.org/software/coreutils/manual/html_node/index.html?utm_source=chatgpt.com "Top (GNU Coreutils 9.7)"
64+
[9]: https://www.gnu.org/s/bash/manual/html_node/Command-Substitution.html?utm_source=chatgpt.com "Command Substitution (Bash Reference Manual)"
65+
[10]: https://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html?utm_source=chatgpt.com "Shell Builtin Commands (Bash Reference Manual)"
66+
[11]: https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html?utm_source=chatgpt.com "Word Splitting (Bash Reference Manual)"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
Here is a precise, end-to-end explanation of what every character in your working line does, why earlier attempts failed, and why this one succeeds.
2+
3+
```powershell
4+
& 'C:\Program Files\Git\bin\bash.exe' --% -lc 'p=$(tr -d "\r" < ~/.claude/prompts/python-developer.md); exec claude --append-system-prompt="$p"'
5+
```
6+
7+
## Stage 1, PowerShell launches Git Bash
8+
9+
1. `&` is PowerShell’s call operator. It tells PowerShell to execute the following string as a program path, even if the string contains spaces. Without `&`, a quoted path would be treated as a plain string, not executed. ([Microsoft Learn][1], [SS64][2])
10+
11+
2. `'C:\Program Files\Git\bin\bash.exe'` is the full path to the Git Bash executable. Quoting the path prevents PowerShell from splitting on spaces.
12+
13+
3. `--%` is the PowerShell stop-parsing token. Everything after `--%` on this line is passed to the native program exactly as typed. PowerShell does not expand `$`, does not interpret `$(...)`, does not treat `"` specially, and does not try to re-tokenize anything. This is the key to avoiding the quoting chaos you were seeing. ([Microsoft Learn][3])
14+
15+
PowerShell therefore runs:
16+
17+
* program: `C:\Program Files\Git\bin\bash.exe`
18+
* arguments, passed verbatim: `-lc 'p=$(tr -d "\r" < ~/.claude/prompts/python-developer.md); exec claude --append-system-prompt="$p"'`
19+
20+
## Stage 2, Bash interprets `-lc '…'`
21+
22+
4. `-l` tells Bash to behave as a login shell. That mainly affects which startup files it reads. It is not essential here, but it is harmless. ([Ask Ubuntu][4])
23+
24+
5. `-c '…'` tells Bash to execute the following command string and then exit. The single quotes here are parsed by Bash, not by PowerShell, because `--%` prevented PowerShell from touching them. ([GNU][5])
25+
26+
So inside Bash, the command string to run is:
27+
28+
```bash
29+
p=$(tr -d "\r" < ~/.claude/prompts/python-developer.md); exec claude --append-system-prompt="$p"
30+
```
31+
32+
## Stage 3, inside Bash, build the argument value
33+
34+
6. `~/.claude/prompts/python-developer.md` uses tilde expansion. In Bash, `~` expands to the current user’s home directory. In Git Bash on Windows the home directory maps to your Windows user profile, for example `/c/Users/Aleksandr`. ([GNU][6], [msys2.org][7])
35+
36+
7. `< file` is input redirection. It feeds the file contents to the left-hand command’s standard input. Here it feeds the file to `tr`. ([GNU][8])
37+
38+
8. `tr -d "\r"` deletes carriage return characters. Windows text files often have CRLF line endings, that is `\r\n`. Removing `\r` leaves clean LF only, which avoids odd parsing errors that can happen when CR characters sneak into quoted strings or command substitutions. ([man7.org][9])
39+
40+
9. `$( … )` is command substitution. Bash runs the command inside the parentheses in a subshell, captures its standard output, strips trailing newlines, and substitutes the result. The result becomes the value assigned to the variable `p`. Embedded newlines are preserved, so the full multi-line file ends up in `p`. ([GNU][10])
41+
42+
After this step, `p` holds the entire prompt file as a single string, with `\r` removed and `\n` intact.
43+
44+
## Stage 4, call the CLI correctly and hand it one argument
45+
46+
10. `exec claude --append-system-prompt="$p"` runs the `claude` program and replaces the current Bash process with it. Using `exec` avoids keeping an extra shell process around, so the interactive session is owned by the CLI. This is conventional when you use a shell wrapper to launch an interactive program. ([man7.org][11])
47+
48+
11. `"${p}"` is double-quoted. Quoting is crucial. In Bash, double quotes prevent word splitting and filename globbing, so the entire multi-line value is passed as a single argument to `--append-system-prompt`. Newlines inside the quotes are preserved and travel as data. Without the quotes, the shell would split on whitespace, which would fragment your prompt into many arguments. ([GNU][12])
49+
50+
12. Why this avoids the infamous `unknown option '---'` error: many system-prompt files start with front-matter like `---`. In your failed attempts, the program wrapper on Windows was mis-tokenizing the multi-line value, so a line starting with dashes was getting interpreted as a new option. In this working form, Bash passes exactly two argv entries to `claude`: the flag name `--append-system-prompt`, then one single argument that contains the entire file. Since the parser sees the flag already, it treats the next token as its value, even if the value begins with a dash. Quoting prevents any intermediate splitting that could have created extra tokens. The behaviour that double quotes stop word splitting is specified in the Bash manual under Word Splitting. ([GNU][12])
51+
52+
## Why the PowerShell, CMD, and mixed-shell attempts failed
53+
54+
* PowerShell by default parses and expands `$`, `$(…)`, quotes, and backticks. When you try to embed a Bash command substitution inside a PowerShell string, you create two different quoting layers that both want to process `$` and quotes. That is why `--%` was necessary, it stops PowerShell from interpreting the rest of the line and lets Bash be the only parser. ([Microsoft Learn][3])
55+
56+
* The `claude.cmd` or `claude.ps1` shims on Windows are not friendly to very long, multi-line arguments. The legacy `cmd.exe` command line length limit and argument handling make this especially brittle, which is why you saw errors like “Too long command line” or spurious option parsing. Launching the real Bash first, then `exec`-ing the CLI from Bash, avoids those layers and their limitations. Microsoft documents the stop-parsing token for exactly these scenarios, where you must pass complex arguments to a native tool without PowerShell interference. ([Microsoft Learn][3])
57+
58+
## Micro-timeline of what happens
59+
60+
1. PowerShell uses `&` to start `bash.exe`. It stops parsing at `--%`, so it forwards `-lc '…'` verbatim. ([Microsoft Learn][1])
61+
2. Bash starts as a login shell because of `-l`, then executes the command string because of `-c`. ([GNU][5])
62+
3. Bash expands `~` to your home, opens the file with `<`, runs `tr -d '\r'`, captures the result with `$( … )`, and assigns it to `p`. ([GNU][6], [man7.org][9])
63+
4. Bash runs `exec claude --append-system-prompt="$p"`. `exec` replaces the shell process with the Claude CLI. The quoted `$p` travels as a single argument that contains the entire multi-line prompt. ([man7.org][11], [GNU][12])
64+
65+
That is why your exact line is reliable: one shell parses once, you normalize line endings, you quote the value so it is one argument, and you avoid Windows shims that mangle long multi-line parameters.
66+
67+
[1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.5&utm_source=chatgpt.com "about_Operators - PowerShell"
68+
[2]: https://ss64.com/ps/call.html?utm_source=chatgpt.com "Call operator - Run - PowerShell"
69+
[3]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.5&utm_source=chatgpt.com "about_Parsing - PowerShell"
70+
[4]: https://askubuntu.com/questions/463462/sequence-of-scripts-sourced-upon-login?utm_source=chatgpt.com "Sequence of scripts sourced upon login"
71+
[5]: https://www.gnu.org/s/bash/manual/html_node/Invoking-Bash.html?utm_source=chatgpt.com "Invoking Bash (Bash Reference Manual)"
72+
[6]: https://www.gnu.org/s/bash/manual/html_node/Tilde-Expansion.html?utm_source=chatgpt.com "Tilde Expansion (Bash Reference Manual)"
73+
[7]: https://www.msys2.org/docs/filesystem-paths/?utm_source=chatgpt.com "Filesystem Paths"
74+
[8]: https://www.gnu.org/s/bash/manual/html_node/Redirections.html?utm_source=chatgpt.com "Redirections (Bash Reference Manual)"
75+
[9]: https://man7.org/linux/man-pages/man1/tr.1.html?utm_source=chatgpt.com "tr(1) - Linux manual page"
76+
[10]: https://www.gnu.org/s/bash/manual/html_node/Command-Substitution.html?utm_source=chatgpt.com "Command Substitution (Bash Reference Manual)"
77+
[11]: https://man7.org/linux/man-pages/man1/exec.1p.html?utm_source=chatgpt.com "exec(1p) - Linux manual page"
78+
[12]: https://www.gnu.org/s/bash/manual/html_node/Word-Splitting.html?utm_source=chatgpt.com "Word Splitting (Bash Reference Manual)"

0 commit comments

Comments
 (0)