Anatomy of a Three-Tier Harness
The structure that binds subagent definitions, a PreToolUse gate, and a policy document into a harness, and the order in which it decides
Blocking edits in the main session also required somewhere to send the work and a rule for choosing that destination. The harness combined role-specific subagents, a PreToolUse gate that limits main-session edits, and a policy document that describes delegation.
flowchart TB
accTitle: The three tiers of the orchestration harness
accDescr: A policy document gives the main session its routing basis, a PreToolUse gate limits the main session's edits, and three gate-exempt subagents plus Codex handle the actual implementation and review.
POLICY[Policy tier<br/>orchestration.md·CLAUDE.md] --> MAIN[Main session<br/>plan·split·delegate·synthesize]
MAIN --> GATE{PreToolUse gate<br/>2 per request·size limit}
GATE -->|blocked| DELEGATE[Refusal message saying to delegate]
DELEGATE --> MAIN
GATE -->|passed| SMALL[Handle trivial edits directly]
MAIN --> DR[deep-reasoner<br/>no write tools·design and review]
MAIN --> DW[default-worker<br/>all write tools·implementation]
MAIN --> TW[task-worker<br/>no Bash·mechanical work]
MAIN --> CX[Codex<br/>bulk generation]
DR -.gate exempt.-> FILES[(Repository files)]
DW -.gate exempt.-> FILES
TW -.gate exempt.-> FILES
CX --> FILES
Tool Permissions for the Three Agents
Claude Code defines subagents with Markdown files placed in ~/.claude/agents/. The frontmatter
carries the name, description, and the model and tools to use; the body describes the role. I kept
three.
deep-reasoner handles design and tradeoff decisions, hard debugging, and review of code written
by Codex. It uses a model strong at reasoning and gets Read, Grep, Glob, and Bash.
default-worker carries a delegated implementation through to the end. It reads code, edits it,
and runs tests. It gets every write tool. task-worker handles formatting, renames, and simple
config changes. It uses the lightest model and does not get Bash.
The two agents use different kinds of restriction. Excluding Bash from task-worker’s tool list
prevents it from calling that tool. deep-reasoner has no Edit or Write, but its Bash access
can still write files, so its limit against implementation depends on the instruction in the
definition body.
You do not implement: return analysis and a recommendation; the
orchestrator routes implementation to a worker.
Review requires reading git diff, so Bash could not be removed. This separates behavior that the
tool list can restrict from behavior that depends only on instructions. I treated those two cases
separately in the gate as well.
When the main session classifies a task, it reads each definition’s description and sends the work
to the matching worker. The deep-reasoner description opens like this.
Use when a task needs hard design or architecture reasoning, tradeoff
analysis, non-trivial debugging that requires sustained thought, or
reviewing Codex-authored diffs.
The File Gate’s Order of Checks
There are three hooks. orch-file-gate.sh looks at Edit and Write tool calls, orch-bash-gate.sh
looks at whether a shell command is trying to write a file, and orch-gate-reset.sh clears the
counter at a request boundary. The first two hang off PreToolUse, the last off UserPromptSubmit.
orch-file-gate.sh, the one that blocks file edits, pulls five things out of the tool-call JSON it
receives on stdin: the caller (agent_id), the session identifier, the target file path, and the
line count and byte count of the content to be written. The byte gate is off by default, so what
was used in the actual decision was the first four. It parses with jq if jq is available, otherwise
with python3. If neither is available it ends with a block.
if command -v jq >/dev/null 2>&1; then
_parsed=$(printf '%s' "$input" | jq -r '...' 2>/dev/null)
elif command -v python3 >/dev/null 2>&1; then
_parsed=$(printf '%s' "$input" | python3 -c '...' 2>/dev/null)
else
printf 'orchestration gate: neither jq nor python3 found; failing closed. Install jq to proceed.\n' >&2
exit 2
fi
Blocking a situation the gate cannot adjudicate is called fail-closed. I used it so a missing parser would not permit an edit.
The gate checks in this order, from the top.
- Kill switch. If
ORCH_GATE_OFF=1or the~/.claude/orch-gate-offfile is present, it passes immediately. This is checked before stdin is read, so recovery is possible even when the payload is broken. - Parsing. A failure blocks.
- Subagent exemption. If
agent_idis present, it passes. That field only arrives when the call originated inside a subagent. - Infrastructure protection. If the main session tries to edit a gate script or a settings file, it is blocked regardless of extension.
- Code file filter. If the target does not have a code extension, it passes.
- Size gate. A single write over 100 lines is blocked. This is checked before the counter is consumed so that a large write does not eat a slot.
- Counter. An edit that reaches this point is counted, and going over two per request blocks.
flowchart TD
accTitle: The file gate's order of checks
accDescr: It checks in the order kill switch, parsing, subagent exemption, infrastructure protection, code extension filter, size gate, and counter, and sends every case it cannot adjudicate to a block.
IN[Edit·Write call] --> KILL{Kill switch on}
KILL -->|Yes| PASS([Pass])
KILL -->|No| PARSE{JSON parsed successfully}
PARSE -->|Failure| BLOCK([Block])
PARSE -->|Success| AGENT{agent_id present}
AGENT -->|Yes subagent| PASS
AGENT -->|No main session| INFRA{Gate infrastructure path}
INFRA -->|Yes| BLOCK
INFRA -->|No| CODE{Code extension}
CODE -->|No docs·config| PASS
CODE -->|Yes| SIZE{Over the line limit}
SIZE -->|Yes| BLOCK
SIZE -->|No| COUNT{Over 2 edits per request}
COUNT -->|Yes| BLOCK
COUNT -->|No| PASS
The subagent exemption sits ahead of the code filter. Workers edit any file without restriction, and the gate squeezes one place only: the main session. Reverse the order and the workers get caught too.
Counting One Request at a Time
The counter is stored in a per-session file. Left as is, the edit count accumulates through the
whole session, so from the second request onward nothing can be edited. The third hook attaches
here. orch-gate-reset.sh hangs off UserPromptSubmit and clears that session’s counter every time
the user sends a new prompt.
If there is no parser or the JSON is broken, the reset hook does not clear the counter. The next request is then blocked sooner. If the file gate instead passes a parse failure, it cannot apply the edit limit.
Counting requires a lock. When edit tools are called back to back within one request, touching the
counter file at the same time throws the value off. The bash that ships with macOS is 3.2 and has
no flock, so I used the atomicity of directory creation as the lock.
until mkdir "$lock_dir" 2>/dev/null; do
_lock_tries=$((_lock_tries + 1))
if [ "$_lock_tries" -ge 25 ]; then
# Stale lock from a crashed hook - remove and take it.
rm -rf "$lock_dir"
if ! mkdir "$lock_dir" 2>/dev/null; then
printf 'orchestration gate: could not acquire counter lock; failing closed.\n' >&2
exit 2
fi
break
fi
sleep 0.2
done
# Release lock on every exit path, including blocked exits.
trap 'rm -rf "$lock_dir"' EXIT
mkdir fails on a directory that already exists. I used that property as the lock, and if it is
still not released after about five seconds, I treat it as a lock left behind by a crash and
reclaim it. trap releases the lock on every path, including a blocking exit.
The Document That Defines What Follows a Block
The gate reports the prohibited call and the reason for refusal. The policy document says who should receive the work after that block.
In ~/.claude/rules/orchestration.md I wrote the main session’s role, routing based on token
economics, and the rule that the two AIs review each other’s code. CLAUDE.md summarizes this policy
so the session always sees it. A session that hits the gate spawns a worker instead of a workaround
because this document told it in advance that delegation was an option.
Installing It and Confirming It Fires
The downloadable bundle contains the three gates I used and the 42-case test suite. Install it into user scope.
curl -fsSLO https://juntiger-assets.pages.dev/agent-harness-gates.zip
unzip -q agent-harness-gates.zip
mkdir -p ~/.claude/hooks
cp agent-harness-gates/orch-*.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/orch-*.sh
Merge the hooks key from settings.example.json into ~/.claude/settings.json and open a new
session. Edit a code file three times in a row from the main session and the tool call is blocked
on the third edit. To turn it off for a while, touch ~/.claude/orch-gate-off; to turn it back on,
delete that file.
The gate’s decisions depend on a few assumptions. If agent_id is present it is a subagent, if the
extension is a code extension it is a code file, counting lines tells you the size. A call for
which an assumption does not hold is not blocked and passes. I asked Codex to search for attack
paths that could bypass those assumptions.
References
Comments
No comments yet. Be the first to leave one.
Pending review