Created
July 30, 2026 14:06
-
-
Save jacobsapps/5f3b7501a169085a3def5bb933a7b98d to your computer and use it in GitHub Desktop.
Agent session finder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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