A unified session browser for coding agents. Search across OpenCode, Claude Code (CLI and desktop app), Claude Cowork, and any other agent with a single fuzzy picker, then resume directly into the session.
brew install --cask dru89/tap/seshDownload a prebuilt binary from GitHub Releases, or install with Go (1.25+):
go install github.com/dru89/sesh/cmd/sesh@latestTo update a non-Homebrew install, run sesh update.
sesh outputs a shell command (cd + resume) that needs to run in your current shell. The easiest way to set this up:
# bash
echo 'eval "$(sesh init bash)"' >> ~/.bashrc
# zsh
echo 'eval "$(sesh init zsh)"' >> ~/.zshrc
# fish
echo 'sesh init fish | source' >> ~/.config/fish/config.fish# PowerShell
echo 'sesh init powershell | Out-String | Invoke-Expression' >> $PROFILEOr source the pre-made wrapper files in shell/ directly. Run sesh init --help to see all options.
sesh # open the picker with all sessions
sesh auth refactor # pre-fill search with "auth refactor"
sesh --agent opencode # only show OpenCode sessions
sesh --json # dump all sessions as JSON (for Raycast, scripts, etc.)
sesh list # non-interactive session list
sesh list --since monday -n 20
sesh show ses_abc # show details for a session (partial ID works)
sesh resume ses_abc # resume a session directly (partial ID works)
sesh stats # session statistics across all agents
sesh setup # detect an LLM CLI and configure AI features
sesh setup --verify # check that the configured commands still work
sesh index # generate summaries for all sessions
sesh index --agent omp # generate summaries for one agent only
sesh recap --days 7 # summarize what you worked on this week
sesh ask "What did I work on around login with claude code?"
In the picker: type to filter, arrow keys to navigate, enter to select, tab to toggle detail pane, esc to cancel.
OpenCode reads ~/.local/share/opencode/opencode.db (SQLite). Pulls session title, slug, working directory, and first user prompts for search.
Claude Code reads ~/.claude/history.jsonl and scans ~/.claude/projects/ for session slugs. Pulls the first prompt text, working directory, and timestamps.
Both providers work automatically if the agent is installed. If the data files don't exist, the provider returns nothing and sesh continues with the others.
Optional. Create ~/.config/sesh/config.json to override resume commands or add external providers. Add the $schema field for autocomplete and validation in your editor:
{
"$schema": "https://raw.githubusercontent.com/dru89/sesh/main/schema.json"
}If you use a wrapper script (like ca) instead of calling the agent binary directly:
{
"providers": {
"opencode": {
"resume_command": "ca opencode -s {{ID}}"
},
"claude-code": {
"resume_command": "ca -r {{ID}}"
}
}
}{{ID}} is replaced with the session ID. The default commands are opencode --session {{ID}} and claude --resume {{ID}}.
The Claude desktop app can run Claude Code sessions from its Claude Code tab. These are real Claude Code sessions against your project directories, but they never show up in the CLI's history file, so sesh surfaces them through a separate claude-code-desktop provider. It reads the app's session metadata — including the session names the app generates, which sesh uses as-is instead of generating its own summaries — and resumes with a plain claude --resume in the right directory, so you can pull a desktop session into your terminal.
The three Claude providers are:
| Provider | What it covers |
|---|---|
claude-code |
Claude Code sessions started in a terminal |
claude-code-desktop |
Claude Code sessions started in the desktop app |
claude-cowork |
Cowork (the desktop app's local agent mode) |
sesh also surfaces Claude Cowork sessions (the desktop app's local agent-mode feature) as a claude-cowork provider, separate from Claude Code CLI sessions. These sessions live inside the desktop app and can't be resumed from a terminal, so the default "resume" command just brings the app to the foreground (open -a Claude on macOS). Override it if you have a better entry point:
{
"providers": {
"claude-cowork": {
"resume_command": "my-claude-launcher {{ID}}"
}
}
}{
"providers": {
"claude-code": {
"enabled": false
}
}
}Any coding agent can integrate with sesh through the external provider protocol. You write a script that outputs JSON, register it in config, and it appears in the picker alongside the built-ins. If you're setting this up from inside a coding agent, see AGENTS.md for instructions you can give it directly.
{
"providers": {
"omp": {
"list_command": ["omp-sesh"],
"resume_command": "omp --resume {{ID}}"
}
}
}list_command is an executable (plus arguments) that outputs a JSON array to stdout. resume_command is a template with {{ID}} and optional {{DIR}} placeholders.
The list command must output a JSON array of session objects:
[
{
"id": "session-id",
"title": "human-readable title or first prompt",
"slug": "optional-short-name",
"created": "2026-01-15T10:30:00Z",
"last_used": "2026-01-15T11:45:00Z",
"directory": "/absolute/path/to/working/directory",
"text": "optional extra searchable text"
}
]| Field | Required | Notes |
|---|---|---|
id |
yes | Whatever the agent uses to identify a session for resuming |
title |
yes | Display name: session title, first prompt (truncated), or slug |
slug |
no | Short human-readable name |
created |
yes | RFC 3339 or Unix milliseconds as string |
last_used |
yes | RFC 3339 or Unix milliseconds as string |
directory |
no | Working directory where the session was started |
text |
no | Additional searchable text (first few prompts work well) |
Rules:
- Exit 0 on success, non-zero on failure
- If no sessions exist, output
[] - Only JSON goes to stdout. Warnings and errors go to stderr.
sesh --json returns an array of all sessions with an added resume_command field:
[
{
"agent": "opencode",
"id": "ses_abc123",
"title": "Fix auth middleware",
"slug": "eager-cactus",
"created": "2026-04-07T09:43:39Z",
"last_used": "2026-04-07T09:47:37Z",
"directory": "/Users/you/project",
"resume_command": "cd /Users/you/project && opencode --session ses_abc123"
}
]This is the integration point for Raycast extensions or other tools that want to present session data in their own UI.
sesh uses LLMs for title generation, session search, recaps, and natural language queries. Each subcommand can use a different model — fast/cheap for high-volume tasks, heavier for prose generation.
sesh setup
Looks for a supported CLI on your PATH, shows you the config it wants to write, and checks that it actually works before finishing. It uses whatever you're already logged into — no API keys to manage.
Found claude on your PATH.
(uses your existing Claude Code login; ~5-10s per summary)
Creating ~/.config/sesh/config.json with:
{
"ask": { "command": ["claude", "-p", "--model", "sonnet", ...] },
"index": { "command": ["claude", "-p", "--model", "haiku", ...] }
}
Write this config? [y/N] y
Wrote ~/.config/sesh/config.json
Checking index, ask.filter_command: claude -p --model haiku ...
ok
Checking ask, recap: claude -p --model sonnet ...
ok
All 2 command(s) working.
It searches for llm, then claude, then codex, preferring llm because it's a single API call rather than a whole agent harness. Existing settings are never overwritten — run it with a partial config and it fills only the gaps.
Run it again any time to check a configuration you already have:
sesh setup --verify # check the configured commands; never writes
sesh setup --yes # skip the confirmation prompt
--verify is the thing to reach for when summaries stop appearing. It runs each configured command against a known test transcript and reports which one is broken and why — which catches an expired login or a retired model far faster than reading config.
Configure one command and everything uses it:
{
"index": {
"command": ["llm", "-m", "haiku"]
}
}The command receives input on stdin and writes output to stdout. Any executable works: llm, claude -p, a script that calls a local model, etc.
Use a fast model for title generation and filtering, a heavier model for prose:
{
"index": {
"command": ["llm", "-m", "haiku"]
},
"ask": {
"command": ["llm", "-m", "sonnet"]
},
"recap": {
"command": ["llm", "-m", "sonnet"]
}
}{
"index": {
"command": ["llm", "-m", "haiku"],
"system_prompt": "You are a session indexer. Output only a short label.",
"prompt": "Label this session in under 15 words."
},
"ask": {
"command": ["llm", "-m", "sonnet"],
"system_prompt": "You are a session search assistant. Answer only from the provided data.",
"prompt": "custom prompt for answer generation",
"filter_command": ["llm", "-m", "haiku"]
},
"recap": {
"command": ["llm", "-m", "sonnet"],
"system_prompt": "You are a work recap assistant. Summarize only the session data provided.",
"prompt": "custom recap prompt"
},
"providers": { ... }
}Each LLM call assembles input from three parts: a system prompt (role framing), the transcript/data, and a task prompt (what to produce). The structure piped to stdin looks like:
[system_prompt]
---
[transcript / session data]
---
[prompt]
system_prompttells the model what role to adopt. The defaults prevent the model from trying to "help with" or "respond to" the session content — a common failure mode when LLMs see conversation transcripts.promptis the task-specific instruction (e.g., "label this session" or "write a recap"). Note it comes after the data, so a custom prompt referring to the transcript's position should say "above" — models told to read a transcript "below" find the instructions there instead and will sometimes reply that no transcript was provided.- If
promptcontains{{TRANSCRIPT}}, the transcript is inserted at that location instead of between the separators. This gives full control over prompt layout, and is how to place the transcript after the instructions if you want that ordering.
Both fields are optional. When omitted, sesh uses built-in defaults with anti-response guardrails.
Each subcommand falls back through other configured commands so you only need to set up the ones you care about:
| Task | Tries in order |
|---|---|
index (title generation) |
index -> recap -> ask -> ask.filter_command |
ask (prose answer) |
ask -> recap -> index |
ask (session filtering) |
ask.filter_command -> index -> ask -> recap |
recap (prose summary) |
recap -> ask -> index |
The pattern: heavy tasks prefer other heavy commands, light tasks prefer other light commands.
sesh can generate one-line summaries for each session. Summaries replace ugly or auto-generated titles in the picker and are included in the fuzzy search corpus.
Bulk (recommended for first run):
sesh index
Shows a progress line per session. Run this once to backfill, then sesh keeps up incrementally.
Lazy background generation: During normal sesh usage, up to 10 unsummarized sessions are processed in the background while the picker is open. Summaries won't appear in the current invocation but will be there next time.
Summaries are generated 4 at a time. This matters most for agent CLIs: llm answers in well under a second, while claude -p boots a whole harness per call and takes 5-10 seconds, which is the difference between a few minutes and half an hour on a large backlog.
Lower it if your command shares a rate limit that parallel calls exhaust:
{
"index": {
"command": ["claude", "-p", "--model", "haiku"],
"concurrency": 2
}
}It only applies to index, the one batch path.
Summaries are cached at ~/.cache/sesh/summaries.json. A cached summary is considered stale when the session's last_used timestamp changes and the summary is more than an hour old. This avoids re-summarizing active sessions on every run while keeping finished sessions up to date.
If summary generation fails (expired credentials, command not found, timeout), sesh continues with the raw title. Nothing crashes.
A single failure is usually transient, so it passes quietly. Repeated total failure is not — it means the configured command is broken and waiting won't fix it — so sesh reports it the next time you open the picker, along with the underlying error:
sesh: summary generation has failed 3 times running.
sesh: last error: command failed: exec: "claude": executable file not found in $PATH
sesh: command: claude -p --model haiku
sesh: run 'sesh setup --verify' to re-check your configuration.
The "run sesh index" hint is suppressed while this is showing, and the whole thing clears itself as soon as generation succeeds again.
Generate a prose summary of what you worked on across all agents over a time period:
sesh recap --days 7 # last 7 days
sesh recap --since monday # since Monday
sesh recap --since 2026-04-01 --until 2026-04-07
sesh recap --agent opencode # only OpenCode sessions
Output goes to stdout as plain text. Use --raw to get the raw markdown without terminal formatting.
When the fuzzy picker returns no results and an LLM command is configured, sesh automatically triggers an AI-powered search. The LLM receives your query along with all session titles/summaries and returns the most relevant matches.
The fallback activates after 3+ characters with no fuzzy matches. A "Searching with AI..." indicator appears while the LLM processes. Results are marked with "(AI)" in the match count. If the LLM call fails, the picker stays on the empty state.
Ask a natural language question about your sessions:
sesh ask "What did I work on around login with claude code since last Monday?"
sesh ask "Show me everything related to the API gateway"
sesh ask --agent opencode "What refactoring have I done recently?"
Uses a two-pass approach: first filters sessions to the relevant subset (fast model), then generates a prose answer from just those sessions (heavy model). Output goes to stdout. Use --raw to get the raw markdown without terminal formatting.
sesh ships with a skill that teaches coding agents (Claude Code, OpenCode, Cursor, etc.) how to search and load past sessions on your behalf. Once installed, your agent can find previous sessions by topic, pull in conversation context, and answer questions like "what did we decide about auth last week?"
npx skills add dru89/sesh -gThe installer will prompt you to choose which agents to configure. You can also target specific agents with --agent claude-code opencode or drop the -g flag to install at the project level instead.
A Raycast extension is included in the raycast/ directory. It provides the same session browsing experience from Raycast's launcher, with configurable terminal support (Terminal.app, iTerm2, Ghostty, Warp, or custom). See raycast/README.md for setup instructions.