Overview

Haunt catches every AI coding session as it ends — Claude Code, Codex CLI, and Cursor — and turns it into clean, searchable markdown. One Python file, zero dependencies, plus a small Mac menu-bar app for config.

Two ingestion tracks, depending on what the source supports:

Single file, zero deps. Everything is in archive.py. Standard library only. No virtualenv. No package manager.

Install

One command. Sign in at /app, copy your personalized install command (your token baked in), paste it into any terminal:

bash
$ curl -fsSL https://haunt-pied.vercel.app/api/v1/install/<your-token> | sh

Wires the Stop hook, schedules the watcher, kicks off a parallel backfill of every existing Claude / Codex / Cursor session, and returns your prompt in about a second. Idempotent — safe to re-run whenever you regenerate a token.

From inside Claude Code

If you're already in a Claude Code session and have the archiver on disk, the slash command shortcut runs the same flow:

claude code
/haunt install

Already have the script?

If ~/.claude/haunt/archive.py exists (you ran the curl install previously), this re-applies everything from scratch. Idempotent — safe to re-run.

bash
$ python3 ~/.claude/haunt/archive.py install
What gets wired: a Stop hook in ~/.claude/settings.json (real-time, async — Claude Code), a watcher LaunchAgent at ~/Library/LaunchAgents/com.haunt.watch.plist (30-second poll — Codex + Cursor), and a one-shot backfill of every session under ~/.claude/projects/, ~/.codex/sessions/, and Cursor's SQLite store. Re-runnable, undo-able with uninstall.

Layout

tree
~/.claude/Haunt/
├── archive.py        # the whole thing — parser + renderer + dispatcher
├── config.json       # backend choice + creds (edit this)
├── state.json        # per-session fingerprints, prevents re-uploads
└── logs/
    ├── archive.log   # operational log
    └── launchd.*     # stdout/stderr from the daily run

Default output (local backend):

tree
~/Documents/claude-logs/
└── 2026-05-10/
    ├── _index.md                                  # daily roll-up with previews
    ├── build-claude-sessions-cloud-backup__24457bf2.md
    ├── add-language-picker-switch__b7d98826.md
    └── …

Choosing a backend

Set backend in ~/.claude/Haunt/config.json. The script auto-creates a default config on first run; edit it whenever you want to switch.

Local folder (default)

json — config.json
{
  "backend": "local",
  "local": { "root": "~/Documents/claude-logs" }
}

Sync the folder however you like — iCloud, Dropbox, Syncthing, a git repo. The archiver doesn't care.

VPS over SSH

json — config.json
{
  "backend": "vps",
  "vps": {
    "user": "abhishek",
    "host": "vps.example.com",
    "remote_root": "~/claude-logs",
    "ssh_key": "~/.ssh/id_ed25519"
  }
}

Uses ssh + scp from your shell. Key-based auth only — the script never prompts. The remote directory is created on first upload.

Vercel endpoint (your own)

json — config.json
{
  "backend": "vercel",
  "vercel": {
    "url": "https://your-app.vercel.app/api/claude-log",
    "token": "shared-secret"
  }
}

Posts a JSON body to your URL with Authorization: Bearer <token>:

json — request body
{
  "path": "2026-05-10/add-language-picker-switch__b7d98826.md",
  "content": "# Add language picker switch\n…"
}

Haunt cloud

Don't want to host anything? Sign in with GitHub at haunt-pied.vercel.app, generate an API token from your dashboard, and paste the snippet below into ~/.claude/haunt/config.json. Everything you archive shows up at /app — searchable, owner-scoped, free while it's in beta.

json — config.json
{
  "backend": "vercel",
  "vercel": {
    "url": "https://haunt-pied.vercel.app/api/v1/sessions",
    "token": "<token from /app>"
  }
}

Adding your own

Backends in archive.py are just functions of the form:

python
def _backend_yourname(rel_path: str, body: str, conf: dict) -> None:
    # rel_path: "2026-05-10/some-session__abcd1234.md"
    # body:     the rendered markdown
    # conf:     config.json["yourname"]
    ...

Add one line to dispatch() and you're done. Around 10 lines for an S3 backend, less for git.

Manual commands

CommandWhat it does
archive.py session <file>Archive one specific JSONL. Used by the Stop hook.
archive.py dailyToday's sessions + write the daily index. Used by the LaunchAgent.
archive.py daily --date 2026-05-09Re-process a specific date.
archive.py backfillEvery session ever recorded. Idempotent.
archive.py … --forceIgnore state.json and re-upload everything.

Config reference

KeyDefaultMeaning
backend"local"local, vps, or vercel.
include_tool_resultstrueEmbed tool stdout in <details> blocks.
tool_result_max_lines40Per-result truncation cap.
skip_short_sessions_turns0Skip sessions with fewer than N user turns.
local_tz"Asia/Kolkata"Timezone for date bucketing & timestamps.

State & idempotency

state.json stores <size>:<mtime> per JSONL path. A session is re-uploaded only when its file grows or changes — typical for sessions that are still active, or that you resume later. Sessions deleted from ~/.claude/projects/ stay archived in your destination.

If something looks wrong, delete state.json and run backfill. It's that simple.

Disable / re-enable

bash
# Pause the watcher (Codex + Cursor stop being polled)
$ launchctl unload ~/Library/LaunchAgents/com.haunt.watch.plist

# Resume it
$ launchctl load -w ~/Library/LaunchAgents/com.haunt.watch.plist

To pause per-session uploads, remove the hooks.Stop block from ~/.claude/settings.json.

Output format

Each session becomes a single markdown file with this shape:

markdown
# Build Claude sessions cloud backup system

- **When:** 2026-05-10 12:58:30 IST
- **Project:** `company` — `/Users/abhishek/Documents/GitHub/company`
- **Branch:** `main`
- **Session ID:** `24457bf2-d3b3-49dc-9573-daa93fe87488`
- **Turns:** 14 user / 20 assistant

---

### User · 12:58:30

build something that automatically takes allllll of my claude sessions…

### Assistant · 12:58:35

Got it. Let me first check how Claude stores sessions on your machine.

**Tool · Bash** — `ls -la ~/.claude/`

```bash
ls -la ~/.claude/
```

<details><summary>Result</summary>

```
total 2200
drwx------   26 abhishek  staff  ...
```
</details>

Troubleshooting

The Stop hook isn't firing

The LaunchAgent isn't running

VPS uploads fail with auth error

I want to start over

bash
$ rm ~/.claude/Haunt/state.json
$ python3 ~/.claude/Haunt/archive.py backfill

Built in one afternoon. Reads JSONL straight out of ~/.claude/projects/ — no API calls, no auth dance. If Claude Code's storage layout ever changes, the parser is ~80 lines.