← Tutorials
BUILD · AGENTS

My scheduled agents ran old code for days and never told me

A crash you notice. An agent that quietly runs stale code and reports OK is the one that costs you four days. The fix is a sync step that is loud when it fails.

For
Anyone running agents on a schedule from a checkout on their own machine
Needs
A scheduled agent (Cowork, cron, CI) and a git repo it runs from
Time
20 minutes

A code-review agent I run every morning reported “no new commits in the last 36 hours.” There were ten. The agent was reading a copy of my repo that had been frozen for four days, and nothing in the whole setup said so. It did not crash. It did not warn me. It just quietly did its job on stale code and reported success.

That is the failure mode nobody warns you about when you start running agents on a schedule. A crash you notice. Stale work that reports OK you do not.

What actually broke

I run several tasks for Aldeia, one of my projects, on a schedule through Cowork: an avisos auto-publisher, a daily code review, a security pass. They all execute from one checkout on my machine at /Users/edra/Documents/Claude/aldeia.

I had shipped an avisos auto-publish rewire two days earlier. It had “never fired.” When I ran the publish manually to debug it, the run hit the old script and wrote a row with status pending into avisos_submissions, instead of pushing the notice live into avisos the way the new code does. The new code was on origin/main. The checkout was not running it.

The tell came from a different agent. That morning’s code-review artifact named a commit hash from four days back and concluded there had been “no commits in 36 hours.” A scheduled agent that confidently describes an old commit as the tip is telling you its runner is stale, if you are paying attention. I was not, for about an hour.

The root cause

The checkout was pinned at f4adaf8, ten commits behind origin/main. Every git pull and git commit there had been failing, silently, since a stale lock file appeared:

$ git -C /Users/edra/Documents/Claude/aldeia rev-parse --short HEAD
f4adaf8
$ git -C /Users/edra/Documents/Claude/aldeia log --oneline HEAD..origin/main | wc -l
10
$ ls -la /Users/edra/Documents/Claude/aldeia/.git/index.lock
-rw-r--r--  1 edra  staff  0 Jun 19 05:34 .git/index.lock

A zero-byte .git/index.lock, dated Jun 19 at 05:34, right in the middle of the morning Cowork window. A git operation got killed mid-write (the Mac went to sleep during a task) and left the lock behind. Git treats that lock as “another git process is working here, back off,” so every later write aborts. Because the lock was never the active lock of a live process, nothing cleaned it up. The checkout sat frozen for four days while the scheduled agents kept running against it.

The fix that is not enough

The obvious fix is one line:

rm -f .git/index.lock && git pull --ff-only

That clears this incident. It does nothing to stop the next one, and it is dangerous as a reflex. Deleting index.lock while a git process is legitimately holding it corrupts your index. So the real rule is: only clear the lock when no git process is alive.

# stale ONLY if the file exists AND no git process is running
[ -f .git/index.lock ] && ! pgrep -f '[g]it ' && rm -f .git/index.lock

But the deeper problem was never the lock. It was that the freeze was silent. The system needs to sync itself before every run, and it needs to be loud when it cannot.

The fix that is

I put a cowork-sync.sh in the repo and made it the first step of every scheduled task. It self-heals the checkout to origin/main, and it exits non-zero on any failure so a broken sync alerts instead of shipping stale code.

The part I want you to copy is the safety design. There are two kinds of checkout, and they need opposite treatment:

I gate the two modes on a gitignored marker file, .cowork-checkout. Marker present means throwaway, force it. Marker absent means shared, sync safely. A destructive reset can therefore only ever touch a checkout I have explicitly tagged as disposable.

#!/usr/bin/env bash
set -euo pipefail
REPO="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$REPO"

# Clear a STALE lock only when no git process is live.
if [ -f .git/index.lock ] && ! pgrep -f '[g]it ' >/dev/null 2>&1; then
  rm -f .git/index.lock
fi

PREV="$(git rev-parse --short HEAD)"
git fetch origin --quiet

if [ -f "$REPO/.cowork-checkout" ]; then
  # Disposable agent checkout — force it to the remote.
  git reset --hard origin/main --quiet
  git clean -fd --quiet
else
  # Shared checkout — safe sync only, stop LOUD if blocked.
  if ! git merge --ff-only origin/main --quiet; then
    echo "cowork-sync: ff-only blocked on $REPO — NOT forcing." >&2
    exit 4
  fi
fi

echo "cowork-sync: OK ${PREV}→$(git rev-parse --short HEAD) (${REPO})"

Two details worth keeping. clean -fd drops untracked files but keeps gitignored ones, so it never deletes your .env.local or node_modules. Never add -x. And if a synced commit changed the lockfile, warn but do not auto-install. npm ci deletes node_modules, which is destructive if yours is symlinked.

The rule

An agent that crashes is a good agent. It tells you something is wrong. An agent that runs stale code and reports success is the one that costs you four days, because it looks exactly like everything is fine.

If you run anything on a schedule from a checkout you also touch by hand, make sync the first step, make failure loud, and never point a reset --hard at a tree that holds work you would miss. The freeze will happen. The only thing you control is whether it stays quiet.

FOUND THIS USEFUL? GET THE NEXT ONE

Drop your email and I'll ping you when the next one goes up, with the tools and workflows I actually use. No spam.

More tutorials ↗