# Lazarus: My Python Apps Now Fix Themselves While I Sleep

By AJTheDev — 2026-07-10
Canonical: https://ajthe.dev/blog/posts/lazarus-self-healing-python.html

---

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:

```python
from lazarus import guard

@guard                      # or @guard(retries=2)
def handle_request(payload):
    ...
```

When the function raises, Lazarus heals in-process, `importlib.reload`s 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](https://discord.gg/d39aaZXAjh). I'm always up for comparing notes on making machines clean up their own mess.
