Skip to content

The Launcher That Pays Twice: Three Guard Layers Before a Delegation Script Fires

Adityo Guni Waluyo

Launching a delegation script twice burns the budget twice. flock, artifact check, and pgrep refuse before anything starts.

TL;DR

The author re-ran a Claude Code command thinking it was stuck, accidentally spawning a duplicate that burned the same budget in parallel. Their launcher now refuses to start using three pre-flight checks: flock -n on a file descriptor, a valid result JSON check, and pgrep -f for a live twin. Each refusal gets a distinct exit code.

Paying twice for one job

The terminal sat quiet. Cursor blinking, no new output from a Claude Code delegation I had started five minutes earlier. My reflex took over: hit Ctrl+C, run the same command again.

That turned out to be an expensive decision. The first session was not stuck, it was thinking hard. Because I re-ran the command, a second session appeared, and the two parallel processes started burning the same max-budget-usd together. The job did not finish faster; I paid twice for the same output.

My first instinct back then: "just add a plain lockfile, or check for a stale PID with ps and pkill whatever is left." Both are brittle. A plain lockfile leaves a corpse behind when the script crashes before cleaning up. And pkill is guesswork: PIDs get recycled, and an old process may already be dead while its state keeps confusing you.

I wanted a mechanism that refuses to run before anything happens, not one that cleans up after a collision. In commit 900f683 on hermes/scripts/cc-launch.sh, the guard stands on three layers of checks, all of them finished before the main command ever starts.

The lock that cannot queue

The core is flock. Plenty of people think bolting on flock is enough; the flag is what decides whether the script lives or dies.

The pattern in the script: exec 9>"$LOCK" then flock -n 9. Why is -n mandatory? Because without it flock waits (blocks) while another process holds the lock. With LOCK_NB, a conflicting request fails immediately and returns EWOULDBLOCK [1]. Without -n, the second launcher quietly queues behind the old process, then fires the moment the first one finishes, and we are back to the same budget problem.

Why lock through a file descriptor instead of relying on the lock file's existence? Because flock locks are tied to the open file description, not to a PID: duplicated fds share the same lock, the lock disappears when all fds close, and it survives execve [1]. With exec 9>"$LOCK", fd 9 lives exactly as long as the launcher process. That is far safer than trusting PIDs the OS may recycle.

Do not repeat finished work

Before refusing, the script needs to know whether the old job actually finished. The second layer inspects the previous run's .out file: if it already contains a valid result JSON, the launcher exits with code 43. When the result exists and is valid, refusing to run again is the cheapest decision available. There is no reason to pay for API tokens twice for the same question, and this is what makes the launcher safe to re-invoke (idempotent).

The third layer connects to that: what if the old process is still running and has not written its result yet? That is where pgrep comes in. By default, pgrep matches its pattern against the process name only; the -f flag is what matches the full command line [3], and only with it can pgrep -f 'claude -p' find anything at all. A match? The launcher exits with code 44: a twin process is alive.

There is a technical nuance here. Some developers are tempted to manipulate process groups with setpgid, but calling it on a child that has already execved fails with EACCES [2]. A small race like this is one more reason every guard check finishes before the child process exists, not after.

Exit codes that tell the story

The end result is a launcher with a firm principle: refuse before starting. Every refusal reason gets its own exit code:

exit 42  # the lock is held by another process
exit 43  # result JSON already present, the work is done
exit 44  # a twin process is still alive

Specific exit codes are far cheaper to debug than letting the collision happen and cleaning up afterwards. I will take a script that fails politely and clearly over one that runs and corrupts system state.

Sources

Related articles