Lazarus: My Python Apps Now Fix Themselves While I Sleep
I built a self-healing runner for Python. When an app crashes, it captures the incident, remembers it in SQLite, and hands it to a headless Claude agent that reads the code, patches the root cause, and reruns to prove the fix held.
I run a lot of Python. Scrapers, batch jobs, little internal servers, half-finished experiments I forgot I left running. And Python being Python, they crash. Usually at the worst possible time — an overnight job that dies at 3am on a KeyError nobody could've predicted, and I find out when I wake up to nothing in the output folder.
So I built Lazarus. It's a self-healing runner. You run your app under it, and when the app crashes it captures the full incident, stores it in a per-project memory, hands it to a headless Claude agent that reads your code — and past related incidents — patches the root cause, then restarts or hot-reloads to verify the fix actually held.
No framework to buy into, no service to sign up for. It's zero dependencies — stdlib only. You need Python 3.11 or newer and the claude CLI. That's it. Because it's pure stdlib it installs into any venv, so I drop it into every project I care about.
The loop, in plain English
Getting started per project is two commands. lazarus init writes a lazarus.toml, creates a .lazarus/ folder and gitignores it. Then lazarus run app.py (or -m mypackage.server) and you're off. Under the hood, here's the actual cycle.
Run. Your program runs as a child process with a crash-reporting hook, breadcrumb capture (last output lines plus log records), and — for batch jobs — an optional dead-man timer that catches hangs and deadlocks by dumping every thread's stack. That last part matters more than it sounds. Half my "crashes" were never crashes; they were silent hangs I'd only notice hours later.
Capture. An unhandled exception writes a structured report: frames, code lines, local variables, breadcrumbs, a fingerprint. And here's the bit I'm quietly proud of — when the failing call's arguments are serialisable, the crash is frozen into a permanent regression test in .lazarus/repro/. Crash-to-test. The bug that just bit you becomes the test that stops it coming back.
Remember. The incident lands in a per-project SQLite database, .lazarus/lazarus.db. Hit the same fingerprint again and it bumps a counter instead of duplicating. Related past incidents get recalled by fuzzy token similarity — not just an exact type-and-file match — and verified fixes from your other projects are pulled in from a shared hive at ~/.lazarus/hive.db. So a fix that worked on one project informs the healer on the next. The tool gets more useful the more it's seen.
Heal. A headless claude -p agent gets Read, Edit, Grep and Glob — plus WebSearch for library errors — and patches the root cause with the smallest safe change. There's a per-attempt model ladder: it can start on haiku for the cheap, obvious stuff and escalate to sonnet then opus when a problem's actually hard. Implicated files get backed up first, and the resulting diff is stored with the attempt. Turn on heal_clones and the agent also greps for the same bug pattern elsewhere and patches the siblings — because you rarely make a mistake exactly once.
Gate. Before a patch counts for anything, the canary runs: your test_command plus every captured repro test. A patch that breaks the tests is marked failed immediately and the next attempt escalates. This is the guardrail that keeps me trusting the thing — a confident-but-wrong fix doesn't get to pretend it worked.
Verify. Lazarus restarts the program. A clean exit, a long healthy run, or even a different crash means the fix is verified — recorded in the hive and written up in .lazarus/PATCHLOG.md with root cause, fix summary and diff. The same crash means the attempt failed, and the next heal is escalated with the failed diff attached: "this was tried; it didn't work." After a configured number of attempts on one error, it gives up and tells you. No infinite patch-crash loops.
One nice touch: transient-looking exceptions like ConnectionError and TimeoutError get a single free backoff-retry before any healing kicks in. Your flaky network is not a code bug, and Lazarus knows better than to rewrite your code because Cloudflare hiccuped.
Healing without a restart
For long-running services where killing the process is a non-starter, you guard the hot path with a decorator instead:
from lazarus import guard
@guard # or @guard(retries=2)
def handle_request(payload):
...
When the function raises, Lazarus heals in-process, importlib.reloads the patched module, and retries the call with the same arguments. The service never goes down. (Functions defined in __main__ can't be reloaded that way — the patch still lands on disk, and lazarus run covers those via restart instead.)
I don't have to trust it blindly
Handing an AI agent write access to your source and telling it "fix things" should make you nervous. It made me nervous, which is why the safety rails came first, not last.
There's a per-error attempt cap, so no runaway loops. Every implicated file is backed up before a single edit. Every attempt's diff is stored, and lazarus show 3 pulls up the full traceback and every fix attempt with its diff — the agent is fully auditable after the fact. The healer gets no shell access by default, and it's explicitly told never to touch .lazarus/, lazarus.toml, or anything you list as protected. And non-Python deaths — a segfault, an OOM kill — are reported, never "healed", because there's no source-level fix for the kernel deciding your process had to go.
Inspecting the memory is just as blunt: lazarus errors lists every incident with status, hit count and location; lazarus hive shows the verified fixes shared across all your projects. It's a black box you can open whenever you want.
The daft bit I love
There's an optional browser extension — the Lazarus Bridge — that lets the healer research through your actual browser instead of a headless search. It opens the top results in background tabs (your active tab and your video are never touched), extracts each page to markdown, saves it under the incident folder, and cites it to the healer before closing its tabs. You can also clip a page yourself — see a relevant GitHub issue, one click, and the healer has it in the next prompt.
And because I clearly have too much free time, it can talk. With a roast personality it checks what you're watching when the crash lands and roasts you for it out loud via the browser's text-to-speech — "Do not pause Never Gonna Give You Up on my account. One of us has a stack trace to read." There's a deadpan mode that keeps the spoken status updates and drops the jokes, if you're a serious person. I am not.
Why I actually built this
The honest version: I was tired of being the exception handler. I'd already seen the same three categories of bug a hundred times, and I was still hand-patching them at stupid hours. The interesting work was never the fix — it was noticing the crash and remembering what I did last time. That's exactly the boring, pattern-matching, memory-heavy job an agent with a good incident database is built for.
So now the pattern-matching is automated and the memory is permanent, and I get to wake up to a PATCHLOG.md that reads like a colleague fixed things overnight and left me notes. Which, functionally, is what happened.
Building your own dev tooling like this — or want a self-healing layer around something flaky you rely on? Come talk to me on Discord. I'm always up for comparing notes on making machines clean up their own mess.
More posts.
Cloning Myself (and Them): Fine-Tuning on My Own Data
Using months of extracted ChatGPT, Claude, and Gemini conversations to fine-tune open models.
The Enshittification of Commercial AI
More guardrails, less context, higher costs — why the big platforms are getting worse.
32 Bits of Authority: On Becoming the Expert I Used to Watch
On authority, systems thinking, and why this is the best era to understand how things connect.