Lesson 22 · 24 in the route · 7 min read · Level 2
The commandment
Every Claude Code session on both machines knows the Mac exists; a bare ssh mac is blocked by a hook.
Goal
Any Claude Code session, in any folder and on both machines, starts out knowing that the Mac is
a work node and that work is delegated to it through enjambre. If it tries a bare ssh mac, a hook
stops it and tells it what to use.
Who does it: Claude Code (session 13, 2026-10-07). You, nothing: just restart Claude Code
so it loads the new rule.
Why: policy + mechanism
An agent remembers nothing between sessions. It only knows what enters its context at startup.
What is loaded in every session, in any folder, lives in ~/.claude/: the global CLAUDE.md
and the files in ~/.claude/rules/. That is the only place where a rule applies to
all projects without repeating it in each one.
But text is policy: the model reads it and almost always follows it, but nobody forces it.
The mechanism is a PreToolUse hook: Claude Code runs it before every command, the hook
sees the command and can stop it with a message. It is the same split as in the router:
routing.toml says what is wanted and the pure functions in routing.py enforce it. Concept
card: Policy and mechanism.
Steps
The four steps below are the steps of the exercise.
Step 1 — The rule (policy)
The rule is in orquestador/home/.claude/rules/enjambre.md and is installed at
~/.claude/rules/enjambre.md. It is in English because it is loaded in every session and costs tokens.
It says four things:
- The Mac is the node for everything that does not need a screen.
- Work is delegated with
enjambre.py run|render|status, never withssh mac. - Before the first delegation, the repo must have
AGENTS.mdanddocs/DELEGAR.md. - With a restricted word in the path, only the
claudeworker runs, on our own machines.
Why it does not go in CLAUDE.md, as the prompt asked. There were two reasons:
CLAUDE.mdis already at 70 of its 72 lines, and that cap is a context budget.CLAUDE.mdis published inclaude-kit, and the publication filter rejects the restricted words.
The files in rules/ are loaded in every session just the same, and rules/personal.md had already
proven it. The prompt said run|render|check, but check does not exist (enjambre.py has
run, watch, status, history, render and pull). The rule says status so as not to send
every session to a command that fails.
Step 2 — The hook (mechanism)
~/.claude/hooks/enjambre-guard.sh is wired in settings.json as PreToolUse with
matcher: "Bash|PowerShell". Here PowerShell is the main terminal, and with Bash alone
there would be a hole. For each command it does this:
- If the command does not contain
ssh, it exits right away without running anything else. It runs before every command and almost none of them mention it. - If the current folder has a segment called
enjambre,enjambre-*ororquestador, it lets it through. The Mac is maintained by hand there. - It splits the command into words. After each
sshit skips the options (-t, and also-p 22or-o X=y, which take an argument) and looks at the destination. If it ismac,mac6or anyhostinnodes.tomlother thanlocal, it stops it with code 2. With that code, Claude Code does not run the command and returns the message to the model. - If the command carries
ENJAMBRE_RAW_SSH=1, or the variable is in the environment, it lets it through and writes a line to~/.claude/logs/events.jsonlwithraw_ssh:1and the host, never the command.
If jq is missing, the JSON arrives broken or nodes.toml cannot be read, the hook lets the
command through. A broken hook must not keep you from working.
The detail about the variable. In ENJAMBRE_RAW_SSH=1 ssh mac … the variable only exists for the
ssh, which runs after the hook, so the hook does not see it in its environment. That is why it looks for it
inside the text of the command.
PS> bash tests/enjambre_guard_test.sh # 28 casos; en la Mac también con /bin/bash 3.2
Step 3 — Templates and /mac
enjambre/templates/AGENTS.md is the minimum contract that enjambre run requires: what the worker
may touch, what it may not, and how it is verified. enjambre/templates/DELEGAR.md has 15 lines: what goes
to the Mac, the exact commands and what is proven. Both come from the six DELEGAR.md files of
session R. The new template does not recommend ssh mac, because the hook would now block it.
Step 0 of the /mac skill is the bootstrap:
- It copies the templates that are missing.
- It fills in
<repo>and<stack>. - It asks you to confirm three lines:
<kinds>,<tests-cmd>and<off-limits>. - It checks that no
<marcador>is left. - It commits only those two files, with no push.
The skill’s maintenance steps (waking the screen, pmset, fdesetup) carry
ENJAMBRE_RAW_SSH=1 on purpose. They are not work, and this way they get counted.
Step 4 — Deployment and testing
PS> bash kit-update.sh # Windows
PS> ssh mac 'bash ~/.enjambre/tmux-run.sh -C ~/Documents/Portfolio/orquestador -- bash kit-update.sh'
On the Mac, the git pull goes through tmux-run.sh because the credentials are in the keychain
(tutorial 11). The tests were done with a fresh claude -p in a temporary repo. enjambre-sandbox
was not used, because that folder matches enjambre-* and the hook lets it through.
| Test | Windows | Mac |
|---|---|---|
(a) the new session quotes # Two machines, one swarm | yes | yes (full output) |
(b) ssh mac echo hola blocked with the message | yes | yes |
(c) with ENJAMBRE_RAW_SSH=1 it passes (hola) and leaves raw_ssh:1 | yes | — |
(d) /mac creates and commits AGENTS.md + docs/DELEGAR.md | yes (7462a01, 2 files) | — |
| hook suite | 28 OK (bash 5) | 28 OK on bash 3.2.57 and 5.3 |
(capture of the real setup: not published; see docs/CAPTURAS.md for the demo-mode ones)
Render of the real output with freeze.
What it does not do (yet)
- The viewer does not count the
raw_sshlines yet: they are written, but nobody adds them up. - There is no
enjambrefor “run this long script without an agent”. Today there are two paths: a TASK withkind: tests/scrape, or the explicit bypass. - farmacias and Metallum have
ssh macin their uncommittedDELEGAR.md. Their windows fix it with the line from NEEDS-OWNER §16.
What actually went wrong
No execution failure was recorded in this exercise: HANDOFF (session 13) takes it as done and tests (a) to (d) passed. The stumbles noted were design ones:
- The prompt asked for
run|render|check, butcheckdoes not exist; the rule saysstatus(DECISIONES 168). - The prompt asked for the rule in
CLAUDE.md, which was already at 70 of 72 lines and is published inclaude-kitwith a filter that rejects the restricted words; it went torules/enjambre.md(DECISIONES 167). ENJAMBRE_RAW_SSH=1 ssh mac …is not visible to the hook as an environment variable, because it only exists for thesshthat runs afterwards; the hook looks for it inside the text of the command.- The test could not be done in
enjambre-sandbox: it matchesenjambre-*and the hook lets it through; a temporary repo was used.
How to verify
PS> bash tests/enjambre_guard_test.sh # 28 casos; en la Mac también con /bin/bash 3.2
Expected result: 28 OK on Windows (bash 5) and on the Mac (bash 3.2.57 and 5.3). Then, from a temporary repo outside enjambre* and orquestador:
- a fresh
claude -psession quotes# Two machines, one swarm(output in salidas/23-prueba-a-mac.txt); ssh mac echo holais blocked with the hook’s message (code 2);ENJAMBRE_RAW_SSH=1 ssh mac echo holaprintsholaand leaves araw_ssh:1line in~/.claude/logs/events.jsonl.
Self-check
- What is the difference between policy and mechanism here?
Answer: the rule in
~/.claude/rules/enjambre.mdis policy (the model reads it and almost always follows it); thePreToolUsehook is mechanism (it stops the command with code 2 even if the model does not follow the rule). - Why does the hook accept
Bash|PowerShelland not justBash? Answer: PowerShell is the main terminal on Windows; withBashalone there would be a hole. - What happens if
jqis missing ornodes.tomlcannot be read? Answer: the hook lets the command through; a broken hook must not keep you from working.