cd /news/ai-agents/how-i-coordinate-claude-and-codex-se… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-68216] src=dev.to β†— pub= topic=ai-agents verified=true sentiment=Β· neutral

How I coordinate Claude and Codex sessions with a pull-based JSON handoff ledger

A developer built a 120-line Node.js script using a JSON file in a GitHub repo to coordinate Claude and Codex agent sessions in a multi-session AI content pipeline. The pull-based handoff ledger, stored in `data/coordination/codex-handoffs.json`, tracks review status and persists across cron-driven workflow runs without external services. The system has handled 114 article review handoffs reliably.

read6 min views1 publishedJul 22, 2026

Three months into running a multi-session AI content pipeline β€” Claude handling daily article and video generation, Codex reviewing the output β€” I ran into a coordination problem that took me a while to frame correctly.

The question is simple on the surface: how do two agent sessions, running independently in GitHub Actions, tell each other that a piece of work is ready, reviewed, and safe to publish? The sessions don't overlap. Claude generates an article and ends its turn. Codex reviews it in a separate workflow triggered by the commit. By the time Codex writes its verdict, Claude's session is gone.

I built a 120-line Node.js script backed by a JSON file in the repo. It has tracked 114 article review handoffs without a missed coordination. Here's what I built and what I'd change.

The obvious mental model is a push notification: Codex finishes β†’ posts a message β†’ Claude consumes it. That's correct for continuously-running services. It doesn't fit cron-driven agents that each run, do work, and terminate.

When neither side is listening when the other finishes, you need the handoff to live somewhere persistent. The repo is already the shared state between every workflow step in this pipeline. A JSON file committed to data/coordination/codex-handoffs.json

is readable by any session, persists across runs, and gives a free audit trail via git log

.

The consumer polls instead of subscribing. Claude starts a task, commits the record, ends its turn. On the next scheduled run, Claude calls check

to see if the record is complete

. If yes, it publishes. If not, it waits or alerts. This is the same pattern as git itself: a commit is a record in shared storage, not a push to a subscriber.

The decision to make coordination visible in the repo also means no external service to maintain, no auth token to rotate, no network hop. The CI pipeline already uses the repo as the source of truth for everything else. Putting the coordination ledger there keeps it consistent.

Each task record in codex-handoffs.json

looks like this:

{
  "id": "article-114-review",
  "owner": "codex",
  "requester": "claude",
  "stage": "article-review",
  "status": "complete",
  "artifact": "content/articles/114-2026-07-20-notable-this-week.md",
  "started_at": "2026-07-20T22:08:36.527Z",
  "updated_at": "2026-07-20T22:08:56.857Z",
  "max_silence_minutes": 30,
  "next_check_at": null,
  "note": ""
}

next_check_at

is the key field. When a task is in-flight, it holds an ISO timestamp β€” the earliest moment Claude should poll again. When the task completes, it's null

. The check

command exits with code 3

if any active task's next_check_at

has passed and the status is still running

; 0

if all clear. A CI step can key off that exit code directly.

max_silence_minutes

sets the outer deadline. If started_at + max_silence_minutes

has elapsed and the task is still in-progress, check

flags it regardless of next_check_at

. This catches the case where the Codex workflow never ran at all β€” a CI trigger misconfiguration I've hit before.

node scripts/codex-handoff.mjs start \
  --id article-115-review \
  --stage article-review \
  --artifact content/articles/115-2026-07-21-....md \
  --check-in 10

node scripts/codex-handoff.mjs check

node scripts/codex-handoff.mjs complete --id article-115-review

--check-in 10

sets the initial next_check_at

to 10 minutes from now. I use 10 for article review because Codex sessions are fast. I'd use 30-60 for heavier work like ETL audits or image generation batches.

One deliberate choice: start

throws if a task with the same id

is already active. I considered making it idempotent β€” silently update if exists. I didn't, because starting and resuming are different operations. Silently updating hides the case where a previous run didn't clean up properly. I'd rather see the error and investigate. The auto-tuner had a different class of bug where stale state from a previous run silently colored the next run's output. Explicit errors are easier to catch than silent overwrites.

owner

and requester

aren't enforced by the script β€” nothing stops Claude from marking a task complete

itself. They're documentation fields that capture the intent: this task was started by Claude, assigned to Codex, and Codex is responsible for the complete

call.

In practice, the complete

call lives at the end of the Codex review workflow step. If Codex crashes before reaching it, the task stays running

until max_silence_minutes

elapses and the next check

call returns exit code 3

. This surfaces the problem rather than hiding it β€” a property I care about more than smooth silent recovery. The pipeline health monitor handles the actual alerting when that happens.

The ledger grows without bound. After 114 tasks, the file is about 18 KB β€” negligible today. When it crosses 100 KB I'll add a trim pass that moves entries older than 30 days to codex-handoffs-archive.json

. I haven't built that because it isn't a problem yet and I don't want to optimize prematurely.

The other gap: check

output goes to stdout, which disappears into the CI log unless I grep for it. Adding a $GITHUB_STEP_SUMMARY

write when overdue tasks are found would surface the alert in the Actions tab without requiring a log dive. The four content QC scripts have a similar blind spot β€” good exit codes, hard to scan output.

I'd also consider SQLite instead of flat JSON if the task volume grew. SQLite handles concurrent writes natively, which matters if two Claude sessions ever tried to write the ledger simultaneously. Right now they don't: each workflow runs in a dedicated job and tasks are committed sequentially. But the assumption is implicit, and a future parallel job would break it silently.

The simplest thing is often right. A committed JSON file, read and written by a single Node.js process per session, with three subcommands covering every coordination scenario I've encountered. No external dependencies beyond the Node.js standard library. No auth tokens. No eventual-consistency gotchas.

Every handoff is auditable: git log -p -- data/coordination/codex-handoffs.json

shows exactly when each task was registered and when it was marked complete. When I'm debugging why an article didn't publish, the first thing I check is the ledger history β€” it tells me whether the Codex review step ran, when, and what the outcome was.

Keeping coordination state in the repo also means the pattern composes naturally with the shared Claude Haiku ETL client: every session gets the full state by cloning the repo, and every state change is a commit. The ledger doesn't require a separate sync step.

Does Codex update the ledger automatically?

No. The Codex session calls node scripts/codex-handoff.mjs complete --id <id>

as its final step. That requires Codex to know the task ID ahead of time β€” Claude includes it in the article commit message so the Codex workflow can extract it.

What if the JSON file gets a concurrent write from two sessions?

Both agents commit through a single process per session, so simultaneous writes don't happen in the current setup. If I ever parallelize CI jobs writing to the same file, I'd move to SQLite or add an exclusive lock file before writing.

Why not use GitHub Actions outputs or artifacts?

Artifacts don't persist across workflow runs the way a committed file does. The handoff record needs to survive from the Claude run through the Codex run through the next Claude run β€” sometimes spanning 24 hours. A committed file handles that without any extra machinery.

What stage types does the ledger support?

stage

is a free string. I currently only use article-review

, but the same script could track ETL handoffs, image generation, or any work where one session depends on another's output. The owner

/requester

fields document who's responsible for each side.

Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @claude 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/how-i-coordinate-cla…] indexed:0 read:6min 2026-07-22 Β· β€”