Stop babysitting your agents
My agent was about to force-push over main. Then my phone buzzed.
The run is frozen mid-command until you tap a button.
Not a "hey, something happened" notification. A decision. The run was frozen — stopped dead mid-command — waiting for me to tap Approve or Deny before it would touch the remote. I was three rooms away from the terminal. I tapped Deny. The force-push never happened, and the agent got a clean "blocked" back and moved on.
Honestly, that's all I want from an agent running on its own — not more output, just a tap on the shoulder: "done!" or "uh, should I?" — so I'm not stuck watching a terminal. Two verbs do it. This post wires both into a real project with godozo, a tiny Apache-2.0 relay that does those two things and nothing else. It's all runnable, so play along.
The setup is outbound-only (Telegram long-polling), so there's no webhook, no inbound port, no cloud anything. It runs on the laptop next to the build.
Warm-up: give the pipeline a voice
Start with the easy half: notifications. I run an autonomous content pipeline (Wendr — it ships scenic-drive regions one at a time, unattended, for hours). For months I checked on it by SSH-ing in and tailing logs like an animal. Now it just tells me.
Each region that goes live sends one line:
godozo notify --title "region live ✅" "Red River Gorge (KY) is live (shard kentucky)"
…and every so often, a heartbeat with the running score:
godozo notify --title "status 🗺️" "🟢 80 live · 57 queued · 0 err"
If a region fails, the same call carries the bad news instead — same channel, so good and bad news land in one place. notify never blocks and never throws into your job; it fans out to every channel you've configured (I mirror to Slack too). That's the whole warm-up — genuinely handy on day one, but let's be honest, anyone can send a push notification. The fun verb is the other one.
Wire it in (paste into Claude Code)
Prompt — set up notifications. Read./llms.txtin the godozo repo. Then: (1) make sure.envhas my Telegram token and chat id and thatgodozo doctorprints a green line; (2) send a test notification with title "godozo test" and body "notifications are wired". Do not tell me you're done until I confirm in this chat that the notification arrived on my phone. Ifdoctorfails, show me the exact error and stop.
The acceptance criterion is the point: the agent doesn't get to claim success, it has to get a human to confirm a real round-trip. That pattern repeats for every step below.
The main event: the blocking decision gate
Notifications are a monologue. The decision gate is a conversation — with a hard stop in the middle. The agent hits something risky, freezes, and asks; nothing moves until you answer. And your answer is literally the return value.
From the command line it's one call, and its exit code is the decision — so it composes with && like anything else in a shell:
# the run stops HERE until you tap a button on your phone godozo gate --title "deploy to prod?" --detail "docker compose up -d" && ./deploy.sh # exit 0 = approved (deploy runs) · 10 = denied · 20 = timed out · 1 = error
Approve on your phone, gate exits 0, the deploy runs. Deny, it exits non-zero and the && short-circuits — the deploy never happens. No dashboard, no polling, no "did the agent remember to wait?" It physically waited.
Making it un-skippable: the seatbelt
Here's the catch. If you just tell the agent "ask before you force-push," that's a policy — and the same model that's meant to follow it is the one deciding whether this particular thing counts. Usually it asks. The one time it doesn't is, naturally, the time you needed it to.
So for the handful of one-way doors — force push, history rewrite, rm -rf, prod deploy — you want something that fires outside the model's control. That's a Claude Code PreToolUse hook. godozo ships a hook-shaped command for exactly this — gate-hook. Drop this in .claude/settings.json:
{ "hooks": { "PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command",
"command": "godozo gate-hook --match \"push --force,push -f,reset --hard,rm -rf\" --title \"Destructive command\"" }] }
]}}What each piece does, because the details are load-bearing:
matcheris a tool name ("Bash"), not a command pattern. It selects which tool to inspect;gate-hook --matchnarrows to which commands prompt. (This trips people up —"Bash(git push*)"is permissions syntax and does nothing in a hook matcher.)gate-hookreads the tool payload from stdin, so your phone shows the exact command —git push --force origin main, not a vague "a Bash command."- It fails closed. Approve → exit
0(the command runs); Deny, timeout, or a dropped connection → exit2, the one code Claude Code treats as a hard block. A seatbelt that fails open is worse than no seatbelt.
One trap worth calling out: a hook built on plaingodozo gateis not safe.gateexits10on deny, and Claude Code treats any non-2 exit as a non-blocking error — it runs the command anyway. You'd approve-test it once, see it work, ship it, and never learn that Deny silently proceeds. Usegate-hookin hooks; usegatein shell&&/||.
Wire it in (paste into Claude Code)
Prompt — install the seatbelt. Read./llms.txt. Add aPreToolUsehook to this project's.claude/settings.jsonthat runsgodozo gate-hook --match "push --force,push -f,reset --hard,rm -rf"with matcher"Bash". Then prove it works without endangering anything: create a throwaway branchscratch/gate-testand attemptgit push --force origin scratch/gate-test. I expect the run to block and my phone to buzz. When I tap Deny, show me that the push did not happen. Don't finish until you've shown me the blocked push.
That acceptance test — force-push a scratch branch and watch it block on Deny — is the one I actually ran while writing this. It's the moment the abstract "human in the loop" becomes a button you press with your thumb.
Make it permanent
Two hook matchers cover the crown jewels. For everything fuzzier — "check with me before anything destructive or long-running" — write it as a standing convention the agent reads every session. Paste this into your project's CLAUDE.md (or AGENTS.md):
## Human-in-the-loop (godozo) - Before any destructive or irreversible action — force push, branch/history rewrite, deleting files outside the working dir, a DB migration, or publishing/deploying — request approval and WAIT for the answer: godozo gate --title "<what>" --detail "<exact command>" Exit 0 = approved (proceed); non-zero = do NOT proceed. - When any task that ran longer than ~5 minutes finishes (or fails), send a one-line summary: godozo notify --title "<task>" "<result>"
The prompt is the policy; the hook is the seatbelt. Write the broad, fuzzy rule in prose, and pin the two or three actions that must never slip with a hook. Don't gate everything — gate the doors you can't walk back through.
Where this is going
Right now the stakes are a force-push — annoying, recoverable. But agents are starting to do things you can't take back: filing the PR, sending the email, and more and more, spending money — buying the compute, paying the API bill, placing the order. Once an agent can spend, "notify me" quietly turns into "get my okay."
Notify and decide is the little atom all of that is built on. The chain is where it goes next — but that's a story for another post. For now: go make your agent ask.
← More from the godozo blog