session-bridge
Local-first, cross-harness agent-session portability. Export a coding-agent session from one harness and resume it in another when the original hits a usage limit or otherwise stops.
Supports three harnesses today:
| Harness | Session store |
|---|---|
| Claude Code | ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl |
| Codex | ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl |
| Hermes | ~/.hermes/sessions/<ts>_<id>.jsonl |
No cloud. Everything runs against files already on your disk.
Supported versions
session-bridge's real dependencies are other tools' storage formats, which can change without notice. This table states exactly what each reader/registrar was last verified against; registration paths validate the store schema at runtime and fail closed (no write) when it doesn't match.
| Harness | Last verified against | Verification |
|---|---|---|
| Claude Code | 2.1.x transcripts | Round-trip + live claude --resume recall of a converted-only fact |
| Codex | Codex CLI 0.145.0 (state_5.sqlite) |
Live codex resume recall of a converted-only sentinel, and the registered session listed in the resume picker |
| Hermes | state.db schema as of 2026-07 |
Live hermes --resume replay of a registered session |
| Python | 3.11 – 3.13 | CI test matrix |
A newer harness version usually still works (formats drift rarely), but treat
anything beyond this table as unverified: run session-bridge inspect first,
and expect SQLite registration to refuse cleanly if the schema moved.
Why
Each harness writes an incompatible session log, and nothing bridges them.
Claude Code /export is lossy plain text, and OpenCode import/export is buggy
across versions. session-bridge normalizes any supported session into one
intermediate representation (IR), then renders it into another harness's shape.
It also carries the pending state (open tool calls, queued input) forward
through a resume handshake, so the receiving agent picks up deliberately
instead of guessing.
How it works
source.jsonl ─▶ reader ─▶ IR (Session: messages, tools, pending) ─▶ writer ─▶ target.jsonl
│
└─▶ resume handshake (prepended system message)
- IR (
ir.py) is the union of what the three harnesses can express: threaded messages with typed content blocks (text / reasoning / tool_call / tool_result), session metadata, tool schemas, and explicit pending state. - Readers (
readers/) normalize each harness into the IR. - Writers (
writers/) render the IR into a target harness and emit aConversionReportnaming every asymmetry that could not transfer losslessly. - Handshake (
handshake.py) turns detected pending state + conversion notes into a resume preamble injected as the first message of the resumed session.
Install
uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
brew install connectwithprakash/tap/session-bridge # or: Homebrew (TUI included)
uvx agent-session-bridge --help # or: no-install run
pip install 'agent-session-bridge[tui]' # or: plain pip
Installs the session-bridge command (docs use that name); an
agent-session-bridge alias is included, so either name runs the same CLI.
Drop [tui] for the CLI without the interactive TUI. From source:
cd session-bridge && uv sync --extra dev --extra tui # or: python3 -m pip install -e .
New here? TUTORIAL.md is a step-by-step walkthrough (find your
session file → inspect → convert → resume) with a real worked example. The
sections below are the quick reference.
Usage
Prefer a guided flow? The interactive TUI discovers sessions across all three stores, walks you through target/options, and shows conversion notes before anything is written:
python3 -m pip install -e '.[tui]' # the TUI needs the optional textual dependency
session-bridge tui
Pick a session, then c to convert (with optional Claude Code placement) or
g to register it into Hermes's state.db or Codex's state_5.sqlite. Both
flows end in a plan screen — loss warnings, backup plan, and the equivalent
CLI command — before any file or database is touched; registration backs up
the store first by default, and the planning phase opens live stores
read-only.
Working with an agent instead? The repo ships an agent skill
(skills/session-handoff) that teaches
any harness's agent to hand off its current session to another harness.
Install it machine-wide into every harness found on the box:
session-bridge install-skill # symlinks into ~/.claude, ~/.codex, ~/.hermes skills dirs
session-bridge install-skill --copy # copies instead (survives uninstall, goes stale on upgrade)
Inspect a session's structure:
session-bridge inspect --from claude-code ~/.claude/projects/<dir>/<uuid>.jsonl
Convert between harnesses:
session-bridge convert --from hermes --to claude-code SESSION.jsonl \
-o resumed.jsonl --handshake-out resume.md
Conversion notes (lossy asymmetries) are printed to stderr; the handshake is
prepended to the output by default (use --no-handshake to disable).
If the source stopped mid-turn with a tool call that never returned, that call
has no result, and a provider rejects a tool call with no matching result on the
next turn (OpenAI Responses returns a 400; Anthropic requires a tool_result).
Pass --stub-open-calls to append a synthetic interrupted result
([session interrupted...], marked as an error) for each genuinely-open call, so
the converted transcript is valid to resume; the report still discloses that the
call was interrupted.
What transfers, and what doesn't
The conversation core (user/assistant text, reasoning summaries, tool calls,
tool results, and call↔result linkage) transfers between all three harnesses.
The following are inherently lossy and are reported per conversion (see
docs/schema-reference.md for the full analysis):
- Thread topology: only Claude Code has
parentUuidbranches. Converting away flattens forks; converting in synthesizes a linear chain. - Reasoning signatures: provider-bound, so reasoning survives as summary text.
- Tool schemas: only Hermes stores them; reconstructed from invoked names otherwise.
- Base/system instructions: only Codex stores them.
- Queued user input: only Claude Code records it, so it surfaces in the handshake.
- Permission/sandbox posture: richest in Codex, absent in Hermes.
- Per-turn model switches: Hermes stores a single session model.
Getting a converted session recognized by the target
How a converted transcript becomes resumable differs per harness. Both cases are
verified on real installs (a session was round-tripped Claude Code → Hermes →
Claude Code and successfully resumed in a live claude process, recalling a fact
that existed only in the converted transcript).
| Harness | How to place it | Resumes from file alone? |
|---|---|---|
| Claude Code (2.1.x) | Write to ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl, then claude --resume <uuid> launched from the matching cwd |
Yes |
| Hermes | Valid filename in ~/.hermes/sessions/ is not enough |
No (needs a SQLite session-store row) |
| Codex | register-codex writes a rollout and its threads index row in state_5.sqlite |
Yes (live-recall verified with Codex CLI 0.145.0) |
Claude Code resolves --resume <uuid> directly from the transcript file. The
one catch: the encoded-cwd directory name must match the directory you launch
claude from (note macOS symlinks like /tmp → /private/tmp; use the real
resolved path). No separate index write is needed. Pass --place-claude-cwd to
convert and session-bridge writes the transcript to the right place and prints
the exact resume command:
session-bridge convert --from hermes --to claude-code SESSION.jsonl \
--place-claude-cwd ~/Developer/myproject
# placed resumable session -> ~/.claude/projects/-Users-you-Developer-myproject/<uuid>.jsonl
# resume with: (cd ~/Developer/myproject && claude --resume <uuid>)
If a transcript already exists at the chosen --session-id, placement fails
rather than silently overwriting a recovered session; pass --force to replace
it deliberately.
Hermes stores sessions in a SQLite database (~/.hermes/state.db), across a
sessions row plus one messages row per turn; the .jsonl files are exports,
not the source of truth. Use session-bridge register to write those rows (it
backs up the DB first):
session-bridge register --from claude-code SESSION.jsonl \
--model moonshotai/kimi-k3 --title "resumed from claude code"
# backed up state.db -> ...
# registered session sb_... into ~/.hermes/state.db
# resume with: hermes --resume sb_...
Verified end-to-end against a real store: hermes --resume replays the registered
history and the model recalls it. Two things matter, both handled by the command:
- a real
started_at(set automatically) so the session isn't sorted below the defaulthermes sessions listlimit; --modelmust name a model Hermes has a provider for. A cross-harness source id (e.g. an Anthropicclaude-*id from a Claude Code session) that Hermes cannot route makes the resumed turn fall back and lose context, so set--modelto a Hermes-configured model.
Codex reading and round-trip are validated against a real tool-using session
(driven through OpenRouter): function_call / function_call_output / reasoning
(both summary and content[] shapes) parse correctly and round-trip identically.
Codex also indexes sessions in SQLite (state_5.sqlite, a threads row with a
rollout_path). Use register-codex to write a converted rollout and index it;
the command validates the local schema, takes a SQLite online-backup before the
mutation, publishes the rollout without overwriting another registration, then
indexes it in SQLite. It removes its own rollout if the index transaction fails;
a process crash can still leave an unindexed rollout, which Codex ignores.
session-bridge register-codex --from hermes SESSION.jsonl \
--cwd ~/Developer/myproject --title "resumed from Hermes"
# backed up Codex state_5.sqlite -> ...
# registered session <uuid> into ~/.codex/state_5.sqlite
# resume with: (cd ~/Developer/myproject && codex resume <uuid>)
This registration path is covered against an isolated Codex-shaped SQLite store
and was live-recall verified with Codex CLI 0.145.0: after codex exec resume
opened an imported session, the model returned a unique sentinel that existed
only in that transcript.
Known limitations
- Codex registration is schema-validated and regression-tested against an isolated
threadsstore; live recall was verified with Codex CLI 0.145.0. Future Codex schema changes still need the same authenticated acceptance check. - Queued-input detection is conservative: it may over-report undelivered input
rather than silently drop it (the safe direction for resume). Enqueue/dequeue
matching is scoped per
sessionId. - Pending-state resumption produces a handshake for a human/agent to act on; it does not itself re-execute open tool calls.
- Failed tool results: Codex and Hermes have no native error flag, so
is_erroris preserved as a[tool error]text prefix (and reported) rather than a field. - Empty-content messages: writers preserve the turn to keep message count stable, but a fully empty turn does not round-trip back through the Codex/Hermes readers' content guards. This is documented, and not observed in real data.
Development
python3 -m pytest # IR, three readers, writers/round-trips, handshake, placement, and both SQLite registrars
Real captured sessions may contain secrets; fixtures/real/ is gitignored and
tests run only against synthetic, faithful fixtures.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_session_bridge-0.2.2.tar.gz.
File metadata
- Download URL: agent_session_bridge-0.2.2.tar.gz
- Upload date:
- Size: 108.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc618be77154c699d63746852ef3a9b6e42923515fa2998de5ebcd27daa09f99
|
|
| MD5 |
4d05fd7bf8778504c7318b4cebc884cf
|
|
| BLAKE2b-256 |
9552ebb314deda849ca50ba90953becfd4f73e8c0a670781ecd0cf931369a901
|
Provenance
The following attestation bundles were made for agent_session_bridge-0.2.2.tar.gz:
Publisher:
release.yml on connectwithprakash/agent-session-bridge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_session_bridge-0.2.2.tar.gz -
Subject digest:
bc618be77154c699d63746852ef3a9b6e42923515fa2998de5ebcd27daa09f99 - Sigstore transparency entry: 2267884923
- Sigstore integration time:
-
Permalink:
connectwithprakash/agent-session-bridge@74fe1902c80eaa42e829975e61aba0e84a2c04a2 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/connectwithprakash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74fe1902c80eaa42e829975e61aba0e84a2c04a2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file agent_session_bridge-0.2.2-py3-none-any.whl.
File metadata
- Download URL: agent_session_bridge-0.2.2-py3-none-any.whl
- Upload date:
- Size: 81.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36c9f95ec456d8fcd60433420612ffaea9eb964287b0772b50dbe13ccb8ab2b9
|
|
| MD5 |
4ac4ec5394441cf930ef06009c762198
|
|
| BLAKE2b-256 |
1d74665f5ea0c2d608bebb0e1c4040b18cc51908f57b78a1a35f5a40b3e2bdbe
|
Provenance
The following attestation bundles were made for agent_session_bridge-0.2.2-py3-none-any.whl:
Publisher:
release.yml on connectwithprakash/agent-session-bridge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_session_bridge-0.2.2-py3-none-any.whl -
Subject digest:
36c9f95ec456d8fcd60433420612ffaea9eb964287b0772b50dbe13ccb8ab2b9 - Sigstore transparency entry: 2267885052
- Sigstore integration time:
-
Permalink:
connectwithprakash/agent-session-bridge@74fe1902c80eaa42e829975e61aba0e84a2c04a2 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/connectwithprakash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74fe1902c80eaa42e829975e61aba0e84a2c04a2 -
Trigger Event:
push
-
Statement type: