Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jacobsapps/5f3b7501a169085a3def5bb933a7b98d to your computer and use it in GitHub Desktop.

Select an option

Save jacobsapps/5f3b7501a169085a3def5bb933a7b98d to your computer and use it in GitHub Desktop.
Agent session finder
# Agent session finder
A way to find and resume any past Claude Code or Codex session without remembering
which folder it was in. Every session logs a one-line record of itself to a single
file as you work; later you (or an agent) grep that file and get back a
copy-pasteable resume command. It's driven entirely by hooks — nothing manual.
## Set it up at the top level
The ledger, the logger script, and the hooks all live in your home directory /
global agent config — **not** inside any one repo. That's the point: the hooks fire
for every session in every checkout, and they all append to the one file at
`~/agent-sessions.md`. So you can launch agents anywhere and query the full history
from anywhere, without caring which project a session belonged to.
## The technique
### 1. Hook into the per-prompt event
Both Claude Code and Codex can run a command on each user prompt, and they hand that
command JSON containing the **session id**, **working directory**, and **prompt
text**. Point both at the same small script.
### 2. Write a small logger that maintains one line per session
- **First prompt of a session** → append a new line: timestamp, tool, cwd, git
branch, the prompt, and a ready-made resume command.
- **Every later prompt** → find that session's existing line by id and rewrite only
a `latest:` segment in place. Each entry then shows where the session started and
where it currently sits, without the file growing per prompt.
A line looks like this:
```
- **2026-06-10 14:11** · **claude** · `/path/to/repo` (`branch`) — first prompt — latest: most recent prompt — resume: `cd /path/to/repo && claude --resume <id>`
```
### 3. Three rules that make it safe to run on every prompt
- **Always exit 0.** A logging failure must never block the agent.
- **Print nothing.** A `UserPromptSubmit` hook's stdout gets injected into the
agent's context, so silence is mandatory.
- **Lock the file** (e.g. `fcntl`) so concurrent sessions don't clobber each other.
### 4. Pick one field separator and sanitize the prompt
The line is delimited by a chosen character (em dash here). Strip that character out
of the prompt text before writing or your fields break. Also collapse whitespace and
truncate long prompts.
## Wiring
**Claude Code** — `~/.claude/settings.json`, a `UserPromptSubmit` hook (reads hook
JSON on stdin):
```json
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "/path/to/agent-session-log claude", "timeout": 10 }
]
}
]
```
**Codex** — `~/.codex/config.toml`, the `notify` hook (gets JSON as the last argv):
```toml
notify = ["/path/to/agent-session-log", "codex"]
```
## Reference logger
A compact Python implementation of the script both hooks point at. No dependencies.
```python
#!/usr/bin/env python3
"""Append one line per agent session to ~/agent-sessions.md.
agent-session-log claude # Claude Code UserPromptSubmit hook, JSON on stdin
agent-session-log codex '<json>' # Codex notify hook, JSON as last argv
Always exits 0 and prints nothing.
"""
import fcntl, json, os, re, shlex, subprocess, sys
from datetime import datetime
LOG = os.path.expanduser("~/agent-sessions.md")
RESUME_SEP = " — resume: "
LATEST_SEP = " — latest: "
def git_branch(cwd):
try:
out = subprocess.run(["git", "-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True, timeout=5)
b = out.stdout.strip()
return b if out.returncode == 0 and b else None
except Exception:
return None
def clean(text, limit=140):
# Em dashes are the field separator within a line, so strip them.
text = re.sub(r"\s+", " ", text or "").replace("`", "'").replace("—", "-").strip()
return text[:limit] + "…" if len(text) > limit else text
def main():
tool = sys.argv[1]
payload = sys.argv[2] if tool == "codex" else sys.stdin.read()
try:
data = json.loads(payload)
except Exception:
return # never block the agent
sid = data.get("session_id") or data.get("id") or ""
cwd = data.get("cwd") or os.getcwd()
prompt = data.get("prompt") or data.get("input") or ""
if not sid:
return
branch = git_branch(cwd)
branch_part = f" (`{branch}`)" if branch else ""
resume = f"claude --resume {sid}" if tool == "claude" else f"codex resume {sid}"
lines = []
if os.path.exists(LOG):
with open(LOG) as f:
lines = f.readlines()
# Already logged this session? Rewrite only its "latest:" segment.
for i, line in enumerate(lines):
if sid in line and RESUME_SEP in line:
head, tail = line.rsplit(RESUME_SEP, 1)
head = head.split(LATEST_SEP, 1)[0]
lines[i] = f"{head}{LATEST_SEP}{clean(prompt)}{RESUME_SEP}{tail}"
break
else:
stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
lines.append(
f"- **{stamp}** · **{tool}** · `{cwd}`{branch_part}"
f" — {clean(prompt)}"
f"{RESUME_SEP}`cd {shlex.quote(cwd)} && {resume}`\n"
)
with open(LOG, "a+") as f:
fcntl.flock(f, fcntl.LOCK_EX)
f.seek(0); f.truncate()
f.writelines(lines)
if __name__ == "__main__":
try:
main()
finally:
sys.exit(0)
```
## The search side
The ledger is just markdown, so finding a session is: grep the file, show matches
newest-first, hand back the resume command. Wrapping that in a skill or slash command
turns it into a natural-language lookup ("which folder was I doing X in?"). For
sessions older than the ledger, fall back to the agents' own on-disk session stores
(Codex's `~/.codex/session_index.jsonl`, Claude's
`~/.claude/projects/<dashed-cwd>/*.jsonl`).
## The point of use
Because it's global, the lookup is conversational from any session:
> "Find me the agent session that ran the review-pass thing and show me how to get back into it."
The agent greps the one top-level ledger and hands back the line, the useful part
being a copy-pasteable resume command:
```
cd /Users/jacob/checkouts/4/granola/ios && codex resume 019ecbcd-2d59-19e0-b8d1-a249622c98a5
```
That one command does both halves of the job: `cd`s into the exact folder the work
happened in, then resumes that specific session id — dropping you straight back where
you left off, full context intact. (Claude sessions are identical, just
`claude --resume <id>`.)
You never touch the id or the path yourself. The hook captured them when the session
started; the finder reads them back on demand. Setting it up once at the top level is
what makes that work everywhere.
## Why it works
The agent tools already persist transcripts and support resume-by-id — but those are
buried per-project and keyed by opaque ids. This adds a single flat, human-readable
index across every checkout, kept current for free because the write is wired to an
event that fires anyway. The whole thing is one hook + one small idempotent script +
a grep.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment