Share

How I Accidentally Used Claude as a Backup System

May 27, 2026 • tech

How I Accidentally Used Claude as a Backup System
claude-codecomposerai-agentsdebugginggitdata-recoveryagentic-development

How I Accidentally Used Claude as a Backup System

A scheduled task in Composer — my homegrown AI agent orchestrator — had run overnight. The job was simple enough to describe in one sentence: build a full Bruno test suite for a third-party golf API. I woke up, checked the dashboard, and there it was: a green checkmark. DONE. A branch name. A started_at, a completed_at. A clean nineteen-minute run.

What it didn't have was a pull request.

That's a problem, because the whole point of Composer's feature workflow is the last mile: design → implement → test → document → commit → open PR. The agent had clearly done the work — the dashboard said so — but the PR never materialized. So I did what felt like the obvious, low-stakes thing. I opened a Claude Code session and said:

"Composer didn't follow our feature workflow — might be a bug with scheduled tasks — and I need the PR created for its work."

Its work. I typed those two words without thinking about them. They carry an assumption: that the work still exists somewhere, sitting on disk, waiting for someone to git push it. I was about to find out how wrong that assumption was — and how a design decision I never made saved me anyway.

The Work That Wasn't There

Every Composer task runs in its own git worktree so that concurrent agents don't trample each other. The task record had the path right there in the database. So that's where Claude started:

$ ls /Users/.../data/worktrees/feature-dashboard-8cb1f6c0
ls: No such file or directory

Gone. The entire directory where every file had been written was cleaned up. This is the moment where, working alone at 7am with coffee in hand, my brain goes straight to "well, the work is lost, I'll just re-run it."

But Claude didn't flinch. It pointed out something I'd half-forgotten: a worktree and a branch are different things. Deleting the working directory doesn't delete the branch ref. The branch still existed. So maybe — maybe — the work was sitting in commits, safe and sound.

That was the first flicker of hope. It was also the first red herring.

Chasing Ghosts

What followed was a methodical walk through every place the work could be hiding. Here's the path the investigation actually took:

flowchart TD
    A["Dashboard says DONE<br/>but there's no PR"] --> B["Check the branch diff"]
    B -->|"1,415 insertions!"| C{"Is any of it<br/>the work?"}
    C -->|"No — stale local main"| D["Check the reflog"]
    D -->|"Branch created,<br/>never moved"| E["Never committed.<br/>Run git fsck"]
    E -->|"No matching objects"| F["Never staged either.<br/>Check the transcript"]
    F -->|"Only summaries,<br/>not the bytes"| G["Check Claude's<br/>session journal"]
    G -->|"Full tool inputs —<br/>and it survived"| H["Replay → reconstruct → PR"]
    style A fill:#fff3cd,stroke:#d39e00,color:#000
    style H fill:#d4edda,stroke:#28a745,color:#000
    style G fill:#cce5ff,stroke:#004085,color:#000

Red herring #1: the diff that lied. A quick git diff --stat against main showed 26 files changed, 1,415 insertions. For about thirty seconds, this looked like a clean win. There's the work!

Except none of it was the work. Every changed file was about an unrelated AI caddie feature. Not a single Bruno test. The tell was in the timestamps — my local main was a full day stale, so the diff wasn't showing the task's work. It was showing every unrelated commit that had landed on the real main in between. The classic three-dot-diff-against-a-stale-base trap. Claude caught it because it checked the base instead of trusting the number.

The reflog tells the truth. When you want to know what a branch actually did — not what a misleading diff implies — the reflog is the source of truth. It records every movement of a ref:

$ git reflog show feature/dashboard-8cb1f6c0
3342b10ce feature/dashboard-8cb1f6c0@{0}: branch: Created from origin/main

One line. The branch was created and never moved. No commit ever advanced it. The work was never committed.

Never even staged. Staged-but-uncommitted content still survives as loose objects in .git/objects, so the next move was to go object-hunting with git fsck — scanning dangling commits and unreachable blobs for anything mentioning the API. Nothing. As far as git was concerned, this work had never existed.

There was one place left: Composer logs every task's conversation to its database. The transcript was right there — 256 rows. You could literally read the agent narrating itself writing each file, running them against the live API, watching the tests pass 4/4. The work definitely happened. But the database stored tool calls as summaries — the literal string [Tool: Write] — not the actual content that went into each file. The transcript proved the work happened. It did not contain the work.

Three dead ends. The directory was gone, git never saw the bytes, and the application log only kept summaries. By every conventional measure, the work was lost.

This is the part I want to dwell on, because it's the whole point. I would have given up two steps earlier. Claude didn't get discouraged, didn't start guessing, and — crucially — didn't fabricate a confident-sounding answer to make the problem go away. Each dead end just became the setup for the next question.

The Backup I Never Set Up

Here's the insight that cracked it.

The task wasn't executed by some abstract "agent." It was executed by Claude Code running as a subprocess inside that worktree. And Claude Code keeps its own session journal — completely separate from Composer's database — under ~/.claude/projects/, in a folder named after the working directory it ran in.

The session ID was sitting right there in Composer's database. One find later:

$ find ~/.claude/projects -name '*f95dc2ba*'
~/.claude/projects/-Users-...-feature-dashboard-8cb1f6c0/f95dc2ba-....jsonl

A 283-line JSONL file. And unlike Composer's summaries, this journal records every message with full tool inputs — the complete content of every Write, the old_string and new_string of every Edit. A quick count:

$ jq -r '.message.content[]? | select(.type=="tool_use") | .name' f95dc2ba....jsonl \
    | sort | uniq -c
   9 Bash
  14 Edit
  21 Read
  32 Write

Thirty-two Writes and fourteen Edits. There was the entire suite — every byte the agent had ever written, fully intact.

Now here's the part that made it recoverable rather than just visible. This journal lives outside the worktree. When the worktree got reclaimed, this file wasn't touched. It sat one directory level up, in Claude's own home, completely indifferent to whatever cleanup had wiped the workspace.

flowchart LR
    subgraph W["The disposable worktree (DELETED)"]
        direction TB
        F["The actual .bru files<br/>— the real bytes"]
    end
    subgraph DB["Composer's database (survived)"]
        direction TB
        S["Tool-call summaries<br/>— proof it happened,<br/>not the content"]
    end
    subgraph CJ["Claude's session journal (survived, lives OUTSIDE the worktree)"]
        direction TB
        J["Full tool inputs<br/>— every Write & Edit,<br/>byte for byte"]
    end
    F -.->|"cleaned up,<br/>bytes gone"| X["❌"]
    S -->|"not enough"| Y["partial"]
    J -->|"everything"| Z["✅ full recovery"]
    style W fill:#f8d7da,stroke:#721c24,color:#000
    style DB fill:#fff3cd,stroke:#856404,color:#000
    style CJ fill:#d4edda,stroke:#155724,color:#000
    style Z fill:#d4edda,stroke:#28a745,color:#000

I never set this up. I never configured a backup for in-flight agent work. But because Claude Code writes a durable, full-fidelity journal that doesn't live inside the disposable thing it operates on, I had one anyway. An accidental backup, created as a side effect of good design.

Replaying the Tape

You can't just dump the 32 Writes to disk and call it done. The agent wrote some files and then Edit-ed them afterward, so a file's final state is its last Write plus every Edit that touched it, applied in order. To recover the exact bytes that were on disk when the tests passed, Claude replayed the mutations chronologically.

A short Python script walks the JSONL top to bottom, keeping a path → content map:

if name == "Write":
    files[path] = inp["content"]
elif name == "Edit":
    old, new = inp["old_string"], inp["new_string"]
    if inp.get("replace_all"):
        files[path] = files[path].replace(old, new)
    else:
        assert files[path].count(old) == 1   # fail loud, don't corrupt silently
        files[path] = files[path].replace(old, new, 1)

I love that assert. If any Edit's old_string didn't match exactly once against the replayed content, the script would crash rather than quietly produce a corrupted file. It's the difference between "I think this is right" and "this is provably right."

The replay finished with zero warnings — every Edit matched exactly once — which is the proof that the reconstruction is faithful, not approximate. Thirty-one files came back. (The 32nd Write was a scratch design note at the worktree root, deliberately excluded.)

Landing It Safely

From there it was ordinary work, done carefully. Recreate the worktree on the original branch so the recovered files land exactly where the dashboard already expected them. Grep the files for the temporary API key that had been in the task prompt — nothing, confirming the agent had correctly used a secret env var instead of hardcoding it. Commit, push, open the PR. Then write the PR number back onto the task row so the system's state finally matched reality.

PR #644: 31 files, 1,305 insertions. The work that had "never existed" was now a clean, reviewable pull request.

What I Actually Learned

Two failures had to stack up for this to happen, and that's the interesting part:

  1. The scheduled-task path skipped the commit/PR phase. The agent did the implementation correctly and even self-validated it — but the run terminated as DONE without ever committing. That's the real bug, and it's now on my list to fix.
  2. Cleanup ran against uncommitted work. Because nothing was committed, when the worktree got reclaimed, the only copy of the files went with it. The cleanup logic assumed "the work is safely in git by now" — an assumption the first bug had quietly invalidated.

Either failure alone would have been survivable. It took both lining up to make the work seem to vanish. That's how most real incidents go — not one dramatic mistake, but two small ones that hold hands at the worst possible moment.

But the lesson that's going to stick with me is this: the most valuable log is the one written by the layer that doesn't get cleaned up. Composer's database had summaries. The worktree had the bytes but got deleted. Claude's sidecar journal had everything and outlived both — not because I planned it, but because it was built to live outside the thing it works on. If you're designing systems that do destructive cleanup, make sure something with full fidelity survives on the other side of the delete.

And then there's the partner angle, which is really why I'm writing this.

I built Composer to be an autonomous system. I like to think of it as pretty solid. It's clearly not flawless — it dropped the ball in two places on the same task. That's fine. Of course it's not flawless; nothing is. What matters is what happens when the autonomous thing fails and a human is standing there at 7am thinking the work is gone.

What happened is that I had a partner who treated panic as just another debugging step. Calm. Methodical. Refusing to guess, refusing to give up, and creative enough to remember a log that lives in a place nobody thinks about. Every dead end I would have read as "it's over," Claude read as "okay, not there — where next?"

The Work Was Never Lost

It was just somewhere nobody had thought to look yet.

That's the line I keep coming back to. The bytes were sitting safely in a file the whole time. What recovered them wasn't a clever git incantation — it was a partner that wouldn't stop following the evidence until it ran out.

I keep building these systems, and they keep surprising me — sometimes by failing, and sometimes by being more resilient than I had any right to expect. Every one of these moments teaches me a little more about where AI actually fits in how I work. This one taught me that the calmest, most methodical member of my team is the one I least expected to need at all.

I'll take that partner into the next storm any day.

–Jeremy


Thanks for reading! I'd love to hear your thoughts.

Have questions, feedback, or just want to say hello? I always enjoy connecting with readers.

Get in Touch

Published on May 27, 2026 in tech