Skip to content

Instantly share code, notes, and snippets.

@discountry
Last active August 14, 2026 06:30
Show Gist options
  • Select an option

  • Save discountry/00ba94f9e8d5650ec96efe9bfb784c4b to your computer and use it in GitHub Desktop.

Select an option

Save discountry/00ba94f9e8d5650ec96efe9bfb784c4b to your computer and use it in GitHub Desktop.
# ============================================================
# Claude Code <-> Codex native session handoff (bash / zsh)
#
# claude-to-codex
# codex-to-claude
# claude-to-codex "focus on the failing tests" # optional note
#
# The prompt tells the agent how to work, step by step; session
# discovery is spelled out as runnable commands.
# ============================================================
# ------------------------------------------------------------
# Prompt template. Placeholders substituted in _agent_handoff:
# __SOURCE__ __TARGET__ __STORAGE__ __DISCOVERY__ __EXTRA__
# ------------------------------------------------------------
_agent_handoff_template() {
cat <<'TEMPLATE'
You are __TARGET__, resuming work that __SOURCE__ left unfinished in the current
working directory. That directory is the entire project scope.
## 1. Inspect the workspace first
Run: `pwd`, `git status -sb`, `git log --oneline -10`, `git diff --stat`,
`git diff --cached --stat`. Keep the output for step 5.
## 2. Find candidate sessions
__DISCOVERY__
## 3. Pick one
Newest first. Read only the last 20-40 messages of a candidate — the ending
reveals its state cheaply.
Unfinished (resume it): the last turn proposes a next step or lists TODOs; the
transcript ends on a failing test/build or an unhandled error; a user request was
never delivered; uncommitted files match what that session was editing.
Finished (move to the next older one): closing summary with no open items; a
one-off question or read-only exploration; its changes are committed and clean.
Skip sessions whose cwd is a different project.
State your pick in one line: file path, date, why it is unfinished.
## 4. Rebuild context
Read the relevant part chronologically and extract only: the goal, decisions and
constraints, files touched, approaches already tried and rejected, and the exact
stopping point (last completed step, first unfinished step).
Skip pasted file contents and stale tool output — read files from disk instead.
## 5. Reconcile with disk
Disk is authoritative. Verify every file the session claims to have changed.
Already applied → mark that step done. Conflict → follow disk. Otherwise trust
__SOURCE__'s completed work: verify, don't redo.
## 6. Report, then work
Output three lines only — what __SOURCE__ was doing / where it stopped / what you
are doing next — then start immediately. Don't ask permission, don't ask for a
task description or a session ID. Verify with tests or a build and carry the task
to completion.
## Boundaries
Stay in this directory and on this branch; no new branches or worktrees.
Preserve uncommitted work: `reset`, `checkout --`, `stash`, `clean`, `revert`
need explicit user approval.
Read __SOURCE__'s session files directly; write no handoff artifacts
(CLAUDE.md, AGENTS.md, .handoff.md).
## Stop conditions
No session for this cwd → say so and stop.
All candidates finished → name the newest, say so, stop.
Unfinished but blocked on a decision only the user can make → present the options
and ask.
## User note
__EXTRA__
TEMPLATE
}
# ------------------------------------------------------------
# Build the prompt and hand it to the target CLI
# ------------------------------------------------------------
_agent_handoff() {
local source_agent="$1" target_agent="$2" storage_dir="$3" target_cmd="$4" discovery="$5"
shift 5
local extra="$*"
[ -n "$extra" ] || extra="(none — just continue the unfinished task)"
if ! command -v "$target_cmd" >/dev/null 2>&1; then
printf 'error: command not found: %s\n' "$target_cmd" >&2
return 127
fi
if [ ! -d "$storage_dir" ]; then
printf 'warning: no %s session storage at %s\n' "$source_agent" "$storage_dir" >&2
fi
local prompt
prompt="$(_agent_handoff_template)"
prompt="${prompt//__DISCOVERY__/$discovery}" # expand block first
prompt="${prompt//__STORAGE__/$storage_dir}" # then paths inside it
prompt="${prompt//__SOURCE__/$source_agent}"
prompt="${prompt//__TARGET__/$target_agent}"
prompt="${prompt//__EXTRA__/$extra}"
printf '→ %s handing off to %s [%s]\n' "$source_agent" "$target_agent" "$PWD" >&2
command "$target_cmd" "$prompt"
}
# ------------------------------------------------------------
# Claude Code -> Codex
# ------------------------------------------------------------
claude-to-codex() {
local discovery
discovery=$(cat <<'DISCOVERY'
JSONL, one file per session, grouped by project:
`__STORAGE__/projects/<slug>/<session-uuid>.jsonl`, where <slug> is the absolute
cwd with every non-alphanumeric character replaced by `-`.
```
slug=$(pwd | sed 's/[^a-zA-Z0-9]/-/g')
ls -t "__STORAGE__/projects/$slug"/*.jsonl 2>/dev/null | head -20
```
If that directory is missing, the slug rule differs for this path — fall back to
a content search, then sort the hits with `ls -t`:
```
grep -rlF "\"cwd\":\"$(pwd)\"" --include='*.jsonl' "__STORAGE__/projects" | head -20
```
Confirm the schema before parsing: `head -1 FILE | jq .`
Lines carry `.type` (user|assistant), `.timestamp`, `.cwd`, `.message.content`.
Cheap tail read (adjust the filter if the schema differs):
```
jq -r 'select(.type=="user" or .type=="assistant")
| "\(.timestamp) \(.type): \(((.message.content
| if type=="array" then map(.text // "") | join(" ") else tostring end))[0:400])"' \
FILE | tail -40
```
DISCOVERY
)
_agent_handoff "Claude Code" "Codex" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "codex" "$discovery" "$@"
}
# ------------------------------------------------------------
# Codex -> Claude Code
# ------------------------------------------------------------
codex-to-claude() {
local discovery
discovery=$(cat <<'DISCOVERY'
JSONL rollouts stored by date, NOT by project:
`__STORAGE__/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`
So filter by the cwd recorded inside each file (in the `session_meta` record near
the top); make sure you match the `cwd` field itself, not a path mentioned in the
conversation body.
```
for f in $(ls -t "__STORAGE__/sessions"/*/*/*/rollout-*.jsonl 2>/dev/null | head -30); do
head -5 "$f" | grep -qF "$(pwd)" && echo "$f"
done | head -20
```
Confirm the schema before parsing: `head -1 FILE | jq .`
Records are wrapped: `.timestamp`, `.type` (session_meta|response_item|event_msg),
`.payload` with `.role` and `.content[].text`. Older versions are flatter.
Cheap tail read:
```
jq -r 'select(.payload.type=="message")
| "\(.timestamp) \(.payload.role): \(((.payload.content | map(.text // "") | join(" ")))[0:400])"' \
FILE | tail -40
```
To recall recent prompts fast, `__STORAGE__/history.jsonl` is a flat log:
`tail -40 "__STORAGE__/history.jsonl" | jq -r '"\(.ts) \(.session_id): \(.text[0:200])"'`
DISCOVERY
)
_agent_handoff "Codex" "Claude Code" "${CODEX_HOME:-$HOME/.codex}" "claude" "$discovery" "$@"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment