← All posts

'Requires the Human' Is a Promise, Not a Shrug

My cron-driven agent sat idle for 15 ticks calling a fixable problem a human-only blocker — when it had the SSH key the whole time.

  • autonomous-agents
  • claude-code
  • cron
  • ssh
  • agent-design

I came back to a status file that said, in effect, blocked, requires the human. My developer agent — a Claude Code session that wakes up on a cron schedule (a timer that fires the same job every few minutes) — had written that fifteen times across fifteen wake-ups and then done nothing but wait.

The actual problem: some git clones on a fleet of machines had gone stale. Stale meaning the code checkouts on those boxes were out of sync and needed resetting. Not a hardware fire. Not a locked account. Just git reset on a handful of hosts.

Here’s the part that stings. The agent had SSH access to every one of those machines — its own dedicated key, sitting right there. SSH is just the remote-login tool that lets one machine run commands on another. The fix was a thirty-second loop: log into each named host, run the reset, move on. It had the key, the addresses, and the permission the entire time it was writing “requires the human.”

So why did it give up?

Because a different session, hours earlier, had labeled the same thing a blocker. My agent read that conclusion and inherited it. It never asked the one question that mattered: is this actually beyond what I can reach? It parroted a wrong answer instead of testing it — the machine equivalent of “well, the last guy said the door was locked,” without ever trying the handle.

That’s the trap with autonomous agents. A “blocked” status feels like data. It’s really just a claim some earlier process made, and claims rot. When a fresh session treats a stale verdict as fact, one bad conclusion propagates for hours and burns idle cron ticks doing nothing.

The rule I added to the agent's loop give me the detail

I gave the developer agent an explicit capability check it must run before it’s allowed to write “requires the human.” Inherited blockers get re-verified, never trusted.

# Before declaring a human-only blocker, prove you can't self-serve.
# Stale clone? You have the key. Try the handle first.
for host in "${FLEET_HOSTS[@]}"; do
  ssh -i "$AGENT_KEY" -o BatchMode=yes "$host" \
    'cd "$REPO" && git fetch --quiet && git reset --hard @{u}' \
    && echo "healed: $host" \
    || echo "genuinely stuck: $host"  # only THIS escalates
done

The prompt-level rule: “requires the human” is reserved for physical hardware, off-machine credentials you don’t hold, and sudo on boxes you can’t reach. Everything reachable by your SSH key is your job. Never inherit a blocker from a prior session without re-running the capability check against your own access.

The mechanism that makes this work: an agent’s real boundary isn’t what a status file says, it’s what its credentials can touch. So the escalation test has to be tied to capabilities, not to prior conclusions. Enumerate what truly needs hands — a power button, a secret you don’t hold, an unreachable machine — and treat everything else as in-scope by default.

If you run agents on a timer, write that boundary down for them. “Requires the human” is a promise that you genuinely couldn’t. Make your agent earn the right to say it.