How the Gate Implementation Changed on Windows
Running the same gate on Windows: the parser structure I changed, how paths and redirects are handled, and the threshold I relaxed
I first built the gate on a Mac, then applied the same rules on a Windows PC running Claude Code. The shell environment was the first incompatibility.
sh Was Missing from the Default Environment
The hooks are sh scripts. The Windows environment I used did not include sh by default, so I
used the one bundled with Git Bash from Git for Windows. In the hook configuration, the script is
passed to sh instead of being executed directly.
{
"type": "command",
"command": "sh \"$HOME/.claude/hooks/orch-file-gate.sh\"",
"timeout": 20
}
On the Mac it was enough to make the script executable and write out the path. The same setup did
not run in the Windows environment I tested. Prefixing the command with sh ran in both of my
environments.
Splitting the Parser Into Its Own File
The Mac version packed parsing into a single shell script. If jq was present it passed a jq
filter; otherwise it passed about ten lines of Python as an argument to python3 -c. Embedding one
language inside a shell string created nested quoting, which made small changes difficult to debug.
In the Windows version I split the decision out into a Python file. The shell script only checks the kill switch, calls Python, and sets an exit code based on the verdict that comes back. The two exchange a single-line verdict.
ALLOW subagent the call came from inside a subagent
ALLOW noncode the target is not a code file
BLOCK size <n> a single write exceeded the line limit
BLOCK badinput the input was malformed
COUNT <sid> a main-session code write. the shell counts it
Splitting the parser made the decision logic directly readable and editable in Python. It also made the Bash gate’s regular-expression checks easier to maintain.
I also changed the order in which the interpreter is looked up. On the Mac, jq was checked first,
but Windows has no jq and is likely to have Python. The lookup goes python3, python, jq, and
blocks if none of the three exists. Python on Windows often has python as its executable name,
so both names are checked.
Different Names for the Same Thing
The Bash gate treats a redirect as a file write. Redirects that discard output are not file writes,
though, so they are exempt. The Mac version exempted three names: /dev/null, /dev/stdout, and
/dev/stderr. Windows uses NUL in the same role, so I added it to the list.
if target.lower() in ("/dev/null", "nul"):
continue
The Windows exception list became narrower: adding NUL dropped /dev/stdout and /dev/stderr,
so commands that redirect to those two are caught as file writes in that version.
Path separators diverged too. The Mac version applied the extension regex to the whole path, but the Python decider takes the file name off the path first. Windows paths arrive with backslashes, so they have to be converted to slashes before the name is extracted.
I also widened the condition that recognizes the Codex executable. A Codex invocation is the
delegation path and must always pass, but on Windows it arrives as codex.exe and sometimes with a
full path attached.
if re.match(r"\s*(\S*[/\\])?codex(\.exe)?(\s|$)", cmd) or "codex-companion.mjs" in cmd:
allow()
Missing any of these differences changes the gate’s decision.
Without NUL, every command that discards logs is caught as a file write and nothing can get done;
without backslash handling, a code file is not recognized as code and slips past the counter.
From 100 Lines to 400
I adjusted the thresholds too. I raised the single-write limit from 100 lines to 400 and rewrote the
list of extensions treated as code. The Mac list was set while building a camera app, so it included
Objective-C and shader extensions. On Windows I was building a game in Lua, and that list did not
fit. I removed the unused m, mm, metal, and glsl entries and added the extensions I needed,
taking the list from twenty-six to thirty-four.
lua c h cc cpp hpp cs js jsx mjs cjs ts tsx py rb go rs java kt swift
sh bash zsh ps1 psm1 php pl sql zig vue svelte html css scss
I remember the 100-line limit tripping often on the Mac. It also blocked legitimate work that writes one large file in full, and each time I had to delegate or turn on the kill switch. After raising it to 400 I ran into that situation less. I did not keep a count of how often it tripped.
I also pulled the values out so they can be changed with environment variables.
GATE_LIMIT="${ORCH_EDIT_LIMIT:-2}"
GATE_MAX_LINES="${ORCH_MAX_WRITE_LINES:-400}" # 0 disables the size sub-gate
Differences Found by Comparing Two Environments
Applying the same rules in two environments let me compare the scope of a false positive. When it reproduced only on the Mac, I checked the Mac configuration first; when it reproduced on both, I checked the shared rule first.
I also rewrote the agent definitions twice around this time. The first wording was specific to the camera app and had the project name baked in, as in “top reasoning specialist for [project name].” When I promoted them to user scope I rewrote the sentences without the repository name, and in the Windows version I rewrote them again to describe what each agent does within the orchestration. I removed those machine-specific project phrases during the port.
References
Comments
No comments yet. Be the first to leave one.
Pending review