@asterzephyr/session-bridge
v0.1.0
Published
Move local coding-agent sessions between Claude Code and Codex CLI.
Maintainers
Readme
session-bridge
Hand off local coding-agent sessions between Claude Code and Codex CLI. Each handoff creates a new session on the target side, preserving conversation history along a shared timeline.
session-bridge does not modify either tool's native sessions in place. It treats a session handoff as a generation: the source session becomes the parent, the new target session becomes the child, and the full chain is recorded for traceability.
Why Generation Handoffs
Most agent tools store sessions in proprietary formats with no official export path. When you need to switch tools mid-task, you lose the conversation that led to your current state: decisions, constraints, context about what was tried.
session-bridge solves this by creating a new session on the target side that contains the transferable content from the source. Both agents can then be used in alternation on the same project, with each handoff recorded as a generation in a shared timeline. This is not the same as modifying a single native thread in place: each generation is a new, independent session file that the target agent discovers normally.
The term "bidirectional" means you can hand off in either direction (Claude to Codex, Codex to Claude), not that both tools share a single session. A round-trip creates two new sessions: one on each side.

Capabilities
- Parse and convert user/assistant messages and tool use/result blocks between Claude Code JSONL and Codex rollout JSONL
- Claude to Codex: delegates to the official
codex app-serverRPC (externalAgentConfig/detect+externalAgentConfig/import), then polls the import ledger and verifies the thread viathread/read - Codex to Claude: writes minimal recoverable Claude Code JSONL atomically, re-parses the output to verify session ID and message count
- Idempotent: same source path + same SHA-256 digest returns the previous result without re-importing
- Generation lineage: same source path with changed content creates a new target session and records it as the next generation on the same timeline
- Handoff capsule: goal, decisions, constraints, changedFiles, validation, openQuestions, nextAction, note; persisted in
~/.session-bridge/state.json - AdapterRegistry with built-in claude/codex adapters; third-party adapters can be registered programmatically
- Lifecycle hooks (opt-in): Claude Code
SessionEndand CodexStophooks queue events for manual or automatic processing - Web UI for browsing timelines, inspecting transfers, and triggering imports
- Doctor command for diagnosing runtime dependencies and hook state
Limitations
- No in-place modification of native sessions. Each handoff creates a new target session.
- No incremental append to an existing target session.
- No conflict resolution or merge between diverged sessions.
- No provider-side prompt cache restoration. First resume after import may behave differently.
- Codex
Stophook fires per turn, not per session exit. In auto mode this triggers a handoff attempt on every turn. - Large session budget: 64 MiB cumulative transferable-message limit per import (fail-closed, no partial import).
- Content not transferred: thinking-block signatures, encrypted content, system/developer messages, permission records, MCP instructions, sidechain/meta messages, file-history snapshots.
- Windows: manual
importworks, buthooks installexplicitly rejects Windows (process.platform === "win32") with a message to use manual imports instead. - The tool is pre-release. It is not published to npm and is not production-stable.
Quick Start
Prerequisites
- Node.js 22 or later
- pnpm 10+
- Claude Code installed (
~/.claude/projects/directory exists) - Codex CLI installed (
~/.codex/sessions/directory exists,codex app-server --stdioresponds)
Install from Source
git clone https://github.com/AsterZephyr/session-bridge.git
cd session-bridge
pnpm install
pnpm buildThe CLI binary is at packages/cli/dist/session-bridge.js. During development:
pnpm dev -- <command> [flags]After the package is published to npm (not yet available):
npm i -g @asterzephyr/session-bridge
session-bridge doctorBack Up First
session-bridge writes new files and calls Codex import RPC. It does not delete or overwrite existing sessions. Back up before first use:
cp -a ~/.claude ~/.claude.bak
cp -a ~/.codex ~/.codex.bak
cp -a ~/.session-bridge ~/.session-bridge.bakCLI Commands
import
Import one session by specifying its source agent and session ID or file path.
session-bridge import --from claude --session <session-id-or-path>
session-bridge import --from codex --session <session-id-or-path>
session-bridge import --from codex --session ~/.codex/sessions/2026/07/13/rollout-xxx.jsonl --dry-run
session-bridge import --from claude --session abc123 --capsule ./handoff.json --note "auth refactor done"| Flag | Required | Description |
|------|----------|-------------|
| --from <agent> | yes | Source agent: claude or codex |
| --session <id-or-path> | yes | Source session ID or JSONL file path |
| --to <agent> | no | Target adapter (default: the other built-in adapter) |
| --capsule <path> | no | JSON file with handoff decisions and constraints |
| --note <text> | no | Short note stored with this generation |
| --dry-run | no | Validate and preview without writing |
| --json | no | Machine-readable JSON output |
sync
Find the latest native (non-bridged) session for the current project and import it.
session-bridge sync --to codex
session-bridge sync --to claude
session-bridge sync --to both --project ~/my-project
session-bridge sync --to codex --dry-run --json| Flag | Required | Description |
|------|----------|-------------|
| --to <agent> | yes | Target: claude, codex, or both |
| --project <path> | no | Project working directory (default: cwd) |
| --note <text> | no | Short note for this handoff |
| --dry-run | no | Validate without writing |
| --json | no | Machine-readable output |
When --to both: finds the latest Claude session and imports to Codex, then finds the latest Codex session and imports to Claude.
list
Show transferable sessions for the current project. Sessions already imported are marked bridged based on the state ledger and a content digest comparison.
session-bridge list
session-bridge list --all-projects
session-bridge list --project ~/my-project --jsonstatus
Check which adapters are available and how many sessions exist.
session-bridge status
session-bridge status --jsondoctor
Diagnose the runtime environment: Node version, claude/codex commands, session directories, hook installation, hook launcher path validity, failed queue state, and pending hook events.
session-bridge doctor
session-bridge doctor --jsonExits with code 1 if any check fails.
hooks
Install, manage, and process lifecycle hooks.
session-bridge hooks install # prompt mode (default)
session-bridge hooks install --mode auto # auto mode
session-bridge hooks uninstall
session-bridge hooks status
session-bridge hooks pending
session-bridge hooks failed # list failed events available for retry
session-bridge hooks run # process oldest pending event (FIFO)
session-bridge hooks run --all # process all pending events
session-bridge hooks run --failed # retry failed events instead of pending
session-bridge hooks run --failed --all # retry all failed eventsui
Launch a local web dashboard.
session-bridge ui
session-bridge ui --port 9000 --no-openOpens at http://127.0.0.1:<port>/#token=<random>. See the Security section.
Hooks
session-bridge can install lifecycle hooks into both Claude Code and Codex so that session handoffs are queued when a session ends.
Claude Code: registers a SessionEnd hook in ~/.claude/settings.json.
Codex: registers a Stop hook in ~/.codex/hooks.json. Codex requires user approval of hooks in its /hooks view before they run.
There are two modes:
- prompt (default): the hook writes minimal event metadata to
~/.session-bridge/hook-events/and exits. You process events later withsession-bridge hooks pendingandsession-bridge hooks run. - auto: the hook enqueues the event and immediately spawns a detached background worker (
session-bridge hook process --event <path>) that runs the import.
Important caveats:
- Codex
Stopis a turn-scope event, not a session-exit event. It fires after every assistant turn. In auto mode, this means a handoff attempt runs after every Codex turn. In prompt mode, events accumulate and you choose which to process. - Install creates a
.session-bridge.bakbackup of the settings file before modifying it. - The hook command uses absolute paths for both the Node runtime and the CLI script (shell-quoted), so it does not depend on
session-bridgebeing in PATH. Thedoctorcommand verifies that the installed launcher paths are still accessible. - Uninstall removes only session-bridge-owned hooks (identified by the
sessionBridge: {owner, version, agent}group marker), preserving any other hooks in the file. Third-party wrapper commands are never claimed or removed based on command suffix matching. - For legacy installations that pre-date the group marker, uninstall also matches by the exact full command string of the current launcher (Node binary + CLI script + arguments). It never matches by command suffix alone, so a wrapper that happens to end with the same script name is not removed.
- session-bridge does not forge Codex hook trust. After installation, open
/hooksin Codex and approve the hook. - The event queue automatically prunes processed events after 7 days and failed events after 30 days. Pending events are never removed automatically.
Web UI
The session-bridge ui command starts a local HTTP server with a single-page dashboard.
Dashboard: displays sessions grouped by project along a handoff timeline. Each entry shows source/target pair, generation number, sync state (pending/synced/warning/orphan), and an import action (shown only for pending state). The server's cwd is auto-selected as the initial project if it has sessions.
Timeline: drill-down view for a single session showing the message sequence with role indicators, timestamps, tool-call collapse, and generation markers. Target sessions hide the already-transferred prefix and display a generation boundary marker, capsule contents, transfer report, and verification warnings. An accessible modal prompts for an optional handoff note before triggering an import.
Settings: read-only view of adapter status, paths, sync rules, and hook configuration.
The UI is dark-only, uses Geist Mono for paths/IDs and Geist for labels, built with React 19, Tailwind CSS 4, and Vite 7. Verified at 1200px and 375px viewports.
How It Works
flowchart TD
subgraph "Claude Code to Codex"
CC[Claude Code JSONL] -->|parse| TS[TransferSession]
TS -->|RPC| AS["codex app-server --stdio"]
AS -->|detect + import| CX[New Codex thread]
CX -->|poll ledger| V1[Verify thread/read]
end
subgraph "Codex to Claude Code"
CXS[Codex JSONL] -->|parse| TS2[TransferSession]
TS2 -->|atomic write| CCT["~/.claude/projects/{path-hash}/{uuid}.jsonl"]
CCT -->|re-parse| V2[Verify sessionId + count]
end
subgraph "State"
V1 --> ST["~/.session-bridge/state.json"]
V2 --> ST
ST -->|"path + SHA-256 = idempotent"| DUP[Return existing result]
ST -->|"path + new digest"| GEN[New generation on same timeline]
endContent Fidelity
| Transferred | Not transferred | |-------------|-----------------| | User messages (full text) | System/developer messages | | Assistant replies (text + tool_use) | Thinking-block signatures | | Tool call inputs and outputs | Encrypted content | | Timestamps | File-history snapshots | | | Permission records, MCP instructions | | | Sidechain/meta messages |
Provider-side prompt cache is not restored. The target session builds its own cache from the first resumed turn onward. The tool does not write or modify any provider-specific metadata fields.
Idempotency
The state ledger at ~/.session-bridge/state.json keys each import by {source-agent}:{resolved-path}:{sha256}.
- Same path, same content: returns the previous result immediately.
- Same path, different content (Claude to Codex): creates a new generation on the same timeline. The Codex app-server import mechanism creates a new thread.
- Same path, different content (Codex to Claude): creates a new target session and records it as the next generation.
Concurrent imports are serialized via an exclusive file lock (state.json.lock) with post-lock state re-check. Lock acquisition polls at 100ms intervals; the lock times out after 90 seconds.
Security and Privacy
session-bridge makes no network requests. The only external process interaction is spawning codex app-server --stdio as a child process over stdin/stdout pipes. Network behavior of that subprocess is outside this tool's control.
File safety:
- Atomic writes for overwritable internal files (state ledger, settings, hook events):
O_EXCLtemp,fsync, thenrenameover the existing path. - Atomic writes for new Claude target sessions:
O_EXCLtemp,fsync, thenlinkto create the target path atomically. If the target already exists,linkreturnsEEXISTand the import fails without overwriting. The temp file is removed after a successful link. - Directories created at mode
0o700, files at0o600. - Source path validated via
lstat: must be a regular file (symlinks rejected). Destination directory validated vialstatto block symlinked directories. Hook event paths checked for root containment (ensureInside). - Source stability checked via stat before and after read/hash; throws if size, mtime, dev, or ino changed.
Web UI server:
- Binds to
127.0.0.1only. - 256-bit random token generated on each startup; passed to the browser via URL fragment, stored in sessionStorage, never sent as a query parameter.
X-Session-Bridge-Tokenheader required on all API requests. Timing-safe comparison.- Host header validated against
127.0.0.1:<port>. - Origin header validated on POST requests.
- CSP:
default-src 'self',frame-ancestors 'none'. Additional headers:X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,Cache-Control: no-store.
State and Backup
All persistent state lives in ~/.session-bridge/:
| Path | Purpose |
|------|---------|
| state.json | Import ledger: timeline generations, capsules, digests |
| state.json.lock | Cross-process exclusive lock |
| hook-events/ | Queued lifecycle hook events (one JSON file per event) |
session-bridge never overwrites or deletes existing Claude or Codex session files that were not created by the current import. Each import writes a new file. If the state ledger write fails after a target session has been created, the behavior depends on the adapter's importSafety declaration: a "rollback" adapter's rollbackImport method removes the just-created target file only if its path and digest still match what was just written, so no orphan accumulates; a "reconcile" adapter does not delete the target but relies on its idempotent reconciliation to avoid duplicates on retry (the target may already exist from a prior attempt). User-owned sessions are never touched in either case.
The state ledger is an atomically rewritten cumulative file; each new import adds to the existing entries. The ledger is bounded at 10 MiB; imports that would exceed this limit are rejected before creating a target session (with a 256 KiB reserve for the result entry).
To reset: delete ~/.session-bridge/. This discards all lineage tracking and deduplication state. Subsequent imports may produce duplicate target sessions for sources that were previously imported. Target session files already written remain in place and are valid sessions in their respective tools.
Architecture
session-bridge/
├── packages/
│ ├── core/ @session-bridge/core (private, zero UI deps)
│ │ ├── adapters/ AdapterRegistry, claude/codex parsers and writers
│ │ ├── bridge/ ImportService, StateStore (dedup + lineage)
│ │ ├── codex/ JSON-RPC client for codex app-server
│ │ ├── hooks/ Hook config (install/uninstall), event queue
│ │ ├── io/ Streaming JSONL reader, atomic file writer
│ │ ├── timeline/ Types, HandoffCapsule, generation lineage
│ │ └── config.ts BridgePaths resolution
│ ├── cli/ @asterzephyr/session-bridge (npm bin)
│ │ ├── bin/ Commander entrypoint
│ │ ├── commands/ import, sync, list, status, doctor, hooks, ui
│ │ └── server/ Hono HTTP server (UI backend)
│ └── ui/ @session-bridge/ui (private, React dashboard)
│ ├── views/ Dashboard, Timeline, Settings
│ └── api.ts Fetch wrapper with token auth
├── testdata/ Sanitized session samples for tests
├── scripts/ Package install smoke test
└── .github/workflows/ CI (Node 22/24) + release (npm trusted publishing)The CLI is bundled with tsup, which inlines @session-bridge/core. Built UI assets are copied into packages/cli/dist/ui. The result is a single npm package.
Development
pnpm install
pnpm check # typecheck + test + build + package smoke test
pnpm dev -- list # run CLI via tsxIndividual packages:
pnpm --filter @session-bridge/core test
pnpm --filter @session-bridge/core typecheck
pnpm --filter @session-bridge/ui build
pnpm --filter @asterzephyr/session-bridge buildVerification
pnpm check runs the full validation gate:
- TypeScript type checking across all packages
- 21 tests: 16 core adapter/import tests + 4 UI timeline model tests + 1 CLI API test
- Production build (tsup bundle + Vite build)
- Package install smoke test:
npm installthe tarball, verify README.md, LICENSE, bundled UIindex.html, and binary version output
CI runs on Node 22 and 24.
Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| CLAUDE_CONFIG_DIR | ~/.claude | Claude Code config root |
| CODEX_HOME | ~/.codex | Codex data root |
| SESSION_BRIDGE_HOME | ~/.session-bridge | State and hook-events location |
| SESSION_BRIDGE_UI_DIR | (auto-detected) | Override bundled UI asset path |
Extending: Writing an Adapter
The AdapterRegistry accepts adapters implementing the SessionAdapter interface:
interface SessionAdapter {
id: AgentId;
displayName: string;
capabilities: AdapterCapabilities;
discover(cwd?: string, limits?: DiscoveryLimits): Promise<SessionMeta[]>;
parse(path: string, options?: SessionParseOptions): Promise<TransferSession>;
importSession?(request: AdapterImportRequest): Promise<AdapterImportResult>;
rollbackImport?(result: AdapterImportResult): Promise<void>;
resumeCommand(sessionId: string): string[];
}
interface AdapterCapabilities {
discover: boolean;
parse: boolean;
importFrom: AgentId[];
resume: boolean;
lifecycleHook: "stable" | "experimental" | "none";
importSafety: "rollback" | "reconcile" | "none";
}Import safety contract: adapters that implement importSession must declare importSafety as either "rollback" or "reconcile". "rollback" requires implementing rollbackImport; the bridge calls it when a post-import state write fails to remove the just-created target. "reconcile" means the adapter handles partial-failure recovery internally via idempotent reconciliation (the target may already exist; nothing is deleted). Read-only adapters that do not implement importSession must declare "none". The registry enforces this constraint at registration time.
Result size limits: AdapterImportResult is serialized into the state ledger. The normalized result must not exceed 64 KiB; the state ledger reserves 256 KiB for the entry (result + lineage metadata). Imports that would exceed these limits are rejected.
To add support for a new agent (Gemini CLI, Cursor, Amp):
- Implement
SessionAdapterinpackages/core/src/adapters/. - The
parsemethod reads the tool's session format and returns aTransferSession. - The
importSessionmethod writes or triggers import on the target side. - Declare
importSafetyin capabilities:"rollback"(must implementrollbackImport) or"reconcile"(adapter handles recovery via idempotent reconciliation). Only read-only adapters withoutimportSessionmay declare"none". - Register your adapter with the registry; extend the CLI to accept the new agent name.
Built-in adapters: claude (lifecycleHook: "stable") and codex (lifecycleHook: "experimental").
Release
The release workflow (.github/workflows/release.yml) triggers on v* tags and publishes to npm using trusted publishing. It consists of two jobs:
- package (no
id-token): checks out code, installs dependencies, runspnpm check(typecheck + test + build + package smoke test), then builds and uploads the release tarball as an artifact. - publish (has
id-token: write, no checkout or dependency install): downloads the verified tarball, verifies that the runner's npm version is >= 11.5.1 (required for trusted publishing), and publishes it.
The tag must match the version in packages/cli/package.json. Node 24 is used for both jobs.
Before npm publish, the package job runs packages/cli/scripts/package-docs.mjs prepare to stage the root README and LICENSE into the package directory. The local test-package script calls the same prepare step then cleans up in a finally block.
Bootstrap (first publish)
npm trusted publishing requires the package to already exist on the registry. Since @asterzephyr/session-bridge has not been published yet, a maintainer must bootstrap the initial version manually:
# 1. Validate everything passes
pnpm check
# 2. Stage docs into the package directory
mkdir -p artifacts
node packages/cli/scripts/package-docs.mjs prepare
# 3. Pack (even if this fails, you MUST run cleanup in step 4)
npm pack --ignore-scripts --pack-destination artifacts ./packages/cli
# 4. Remove staged docs (required even if pack failed)
node packages/cli/scripts/package-docs.mjs cleanup
# 5. Publish the tarball
npm login
npm publish ./artifacts/asterzephyr-session-bridge-0.1.0.tgz --ignore-scripts --access publicAfter the initial publish, configure the trusted publisher mapping on npmjs.com to point at the AsterZephyr/session-bridge repository's release.yml workflow and grant it publish permission. Subsequent releases triggered by v* tags use trusted publishing with no manual credentials. The workflow is not functional until the bootstrap publish and trusted publisher configuration are both complete.
Roadmap
Planned directions, no committed timeline:
v0.2
- Hook coalescing: debounce Codex per-turn Stop events so auto mode does not attempt a handoff after every turn
- Generation diff: show what changed between consecutive generations of the same timeline
- Capsule editor: interactive CLI and UI for editing handoff capsule fields before confirming an import
- Capsule export: Markdown or JSON export of a timeline's full capsule chain
v0.3
- Adapter SDK: schema-based compatibility checking for third-party adapters
- Format contract and canary compatibility tests: validate adapter parse/import against versioned fixture snapshots to catch format drift
- Windows hooks support and full path separator testing
- Optional encrypted local metadata (encrypt capsule content at rest)
- Generation compaction: squash intermediate generations into a single summary generation
