Build a Scheduled Bug-Fixing Agent with Claude Code and Your Error Logs

There's a category of bug that never gets fixed on purpose. Not the outage that pages you, and not the feature-blocking defect a customer emails about — the quiet ones. A TypeError that fires forty times a day in a code path you haven't touched in months. A timeout that only hits users on slow connections. They sit in your error tracker accumulating counts, and every time you glance at them you make the same calculation: real, but not worth an afternoon.
Coding agents changed that calculation. Claude Code can run headless from a script, read a repo, write code, and open a pull request. Which means the marginal cost of attempting a fix for one of those quiet bugs is no longer your afternoon — it's a few minutes of compute while you sleep.
This post walks through the architecture I'd actually build: error logs in, pull requests out, human review in the middle. It's concrete enough to implement in a weekend, and I'll be honest at the end about what goes wrong.
The architecture in one paragraph
Your app ships errors to a log store that fingerprints them — collapsing thousands of raw events into a handful of distinct error groups. A nightly scheduled job asks the store: which fingerprints are new since yesterday? For each new group (capped at a small number), it spawns a headless Claude Code session with the error details, stack trace, and a fresh checkout of the repo. The agent tries to reproduce the bug with a failing test, drafts a fix on a branch, and opens a PR. In the morning, you review PRs instead of stack traces.
The three load-bearing pieces are fingerprinting, the loop, and the guardrails. Let's take them in order.
Piece 1: Fingerprinted errors, not raw logs
Raw log lines are useless as agent input — the same bug produces thousands of near-identical events that differ only in IDs, emails, and timestamps. What you want is a stable hash over the shape of the error:
// Normalize away the variable parts, hash what remains.
function fingerprint(level: string, scope: string, message: string) {
const normalized = message
.replace(/[0-9a-f]{8}-[0-9a-f-]{28}/gi, '<uuid>')
.replace(/\S+@\S+\.\S+/g, '<email>')
.replace(/\d+/g, '<n>')
return sha256(`${level}|${scope}|${normalized}`)
}
Now Timeout fetching order 8231 for anna@example.com and Timeout fetching order 114 for raj@example.com are one group with a count of two, not two unrelated lines. Sentry does this out of the box; so does the logs feature in Helmdesk, which rolls events up into issues keyed by exactly this kind of fingerprint. If you're on plain text logs, the normalization function above gets you 90% of the way.
The fingerprint is what makes "only new errors" a queryable condition — and that condition is the difference between an agent that investigates genuine regressions and one that burns tokens re-litigating a known flaky timeout every single night.

Piece 2: The nightly loop
Here's the skeleton, runnable as a cron job or a GitHub Actions schedule workflow:
#!/usr/bin/env bash
set -euo pipefail
MAX_FIXES_PER_NIGHT=3
# 1. Ask your log store for error groups first seen in the last 24h.
NEW_ISSUES=$(curl -s "$LOGS_API/issues?level=error&firstSeenSince=24h" \
-H "Authorization: Bearer $LOGS_API_KEY" \
| jq -c ".issues[:${MAX_FIXES_PER_NIGHT}][]")
for issue in $NEW_ISSUES; do
FINGERPRINT=$(echo "$issue" | jq -r .fingerprint)
# 2. Skip anything we've already attempted (attempted != fixed).
if git ls-remote --heads origin "agent/fix-${FINGERPRINT:0:12}" | grep -q .; then
continue
fi
# 3. Fresh checkout, fresh branch.
git clone --depth 50 "$REPO_URL" "/tmp/fix-$FINGERPRINT" && cd "$_"
git checkout -b "agent/fix-${FINGERPRINT:0:12}"
# 4. Hand it to Claude Code, headless.
claude -p "$(build_prompt "$issue")" \
--allowedTools "Read,Edit,Write,Bash(npm test:*),Bash(git *)" \
--max-turns 40
# 5. Only open a PR if the agent left a commit behind.
if [ -n "$(git log origin/main..HEAD --oneline)" ]; then
git push -u origin HEAD
gh pr create --draft \
--title "fix: $(echo "$issue" | jq -r .title)" \
--body "Automated fix attempt for fingerprint ${FINGERPRINT:0:12}. Review carefully."
fi
done
The prompt is where you encode your engineering standards. Mine looks roughly like this:
You are fixing a production error. Details:
Message: {{message}}
Stack trace: {{stack}}
Occurrences: {{count}} since {{firstSeen}}
Scope: {{scope}}
Rules:
1. FIRST, write a failing test that reproduces this error. If you
cannot reproduce it after a genuine attempt, STOP — write your
findings to INVESTIGATION.md and make no other changes.
2. Then make the smallest change that turns the test green.
3. Run the full test suite. Do not commit if anything else broke.
4. Commit with a message explaining the root cause, not just the fix.
Rule 1 is the whole game. An agent that must reproduce before it repairs can't hallucinate a fix for a bug it doesn't understand — the failing test is proof of understanding, and it rides along in the PR as proof for the reviewer too.
Piece 3: Guardrails
These aren't optional hardening; they're what makes the system safe to leave running unattended.
- Never auto-merge. Ever. The agent opens draft PRs. A human merges. The moment you're tempted to auto-merge "just the simple ones," remember that the agent is the one deciding what's simple.
- Cap attempts per night. Three is plenty. A deploy that spawns twenty new fingerprints is an incident, not twenty agent tasks — a cap keeps a bad night from becoming a bad bill.
- Only new fingerprints. Recurring known errors are a triage decision for you, not fresh work for the agent. The branch-existence check above also stops it retrying the same bug nightly.
- Budget ceilings. Bound each session with
--max-turns(or a token budget) so one pathological bug can't consume the night. Add a monthly spend alarm on the API key. - Scoped credentials. The agent's key can read code, run tests, push branches, and open PRs. It cannot merge, deploy, touch secrets, or write to
main. Branch protection enforces this even if the prompt fails to. - CI is the second reviewer. The PR runs your normal pipeline. An agent fix that breaks lint or an unrelated test gets flagged before you've had coffee.
What goes wrong (honestly)
Run this for a few weeks and you'll hit all of these:
Reproduction is the bottleneck. Errors that depend on production data shape, third-party outages, or race conditions often can't be reproduced from a stack trace in a clean checkout. Expect a meaningful share of runs to end in INVESTIGATION.md instead of a fix. That's a feature — a written investigation with the relevant files identified is still a real head start — but if you expected a 90% fix rate, recalibrate.
Plausible-but-wrong fixes. The classic failure is treating the symptom: wrapping the crash site in a null check when the real bug is upstream where the null was created. The failing-test rule catches the worst of it, but you still need to review these PRs like you'd review a well-meaning intern's — the code will look confident either way.
Flaky tests poison everything. If your suite has a 5% random failure rate, the agent will either abandon good fixes ("something else broke") or learn to distrust the signal. Fix your flakes before you deploy this, not after.
Noisy fingerprinting creates noisy work. If your normalization is too loose, one bug becomes five "new" groups and the agent duplicates effort. Too tight, and distinct bugs merge. Budget an hour to tune it against a week of real data.
None of these are reasons not to build it. They're reasons to keep the human review step sacred and to measure the system by a modest bar: does it hand you at least one genuinely mergeable PR a week for bugs you'd otherwise never have scheduled? In my experience, that bar clears — and the quiet bugs finally stop accumulating.
Give your agent something to read
Ship your app's errors to fingerprinted, queryable log issues — new-error detection, retention tiers, and an API your nightly job can poll. Free to start.