Back up your Claude Code, Codex, Gemini CLI, Qwen Code, OpenCode, Copilot CLI and Cursor sessions
A session is a saved conversation: messages, tool calls, output, and the working directory. A backup is a copy the tool can reopen, not an export you read in a text editor; that matters when a session disappears.
All seven write sessions to disk unencrypted: Claude Code, Codex, Gemini CLI, Qwen Code, GitHub Copilot CLI and Cursor as plaintext JSONL — one JSON object per line for each message, tool call or result, with Copilot CLI keeping one event log and workspace files per session — and OpenCode as one SQLite database. Prompts, code, output, paths and secrets are in the file, so protect a backup like the machine. Encrypt it, keep unencrypted copies out of public repositories and shared sync folders, and test the restore; the secrets guide covers what tends to end up in these files.
Status, 27 October 2026: retention details are in the table below, and hook events come from each tool's reference, not a test run here. Cursor's storage paths are staff-reported rather than documented, and Cursor 3.27 was the newest editor build on 27 October 2026. Gemini CLI's SessionEnd hook is best effort: the CLI does not wait for it, so treat its copy as a bonus. Qwen Code has no documented age-based session sweep; its general.cleanupPeriodDays covers file-history backups and debug logs, not the transcripts. Copilot CLI sessions are also synced to your GitHub account by default, subject to your organization's policy; that copy is separate from anything you back up locally.
A backup has to be readable by the tool. /export in Claude Code, Codex and Qwen Code, /share in Copilot CLI, /chat share in Gemini CLI, and Export Transcript in Cursor write a reading copy, not a resume file; OpenCode's JSON export is importable as a backup. The export guide covers the readable side.
Checked against each tool's documentation on 27 October 2026.
Where the sessions are, and what deletes them#
| Tool | Local store | What removes it |
|---|---|---|
| Claude Code | ~/.claude/projects/<project>/<session-id>.jsonl (where transcripts are stored), plus a <session-id>/ folder for subagents and large tool outputs | The 30-day retention sweep, claude purge, or deleting the files yourself. Cloud sessions are separate. |
| Codex | Codex's session directories: $CODEX_HOME/sessions and $CODEX_HOME/archived_sessions (default ~/.codex) | codex delete or manual file removal. Archiving only moves a chat; the docs describe no age-based sweep. |
| Gemini CLI | ~/.gemini/tmp/<project-id>/chats/session-*.jsonl, one file per session (session management); <project-id> is mapped in ~/.gemini/projects.json | gemini --delete-session <index>, x in the /resume browser, or the default 30-day retention cleanup (general.sessionRetention). |
| Qwen Code | ~/.qwen/projects/<sanitized-cwd>/chats/<session-id>.jsonl (session data), with live-session .runtime.json sidecars in the same folder | /delete inside a session, manual file removal, or a project rename or move, which makes the store unreachable. No age-based sweep is documented; general.cleanupPeriodDays covers file-history/ and debug/, not chats/. |
| OpenCode | ~/.local/share/opencode/opencode.db (V1 and V2), or the path the CLI prints — V2: opencode debug paths db; V1: opencode db path | opencode session delete, a V1 uninstall without --keep-data, or undocumented pruning. |
| GitHub Copilot CLI | ~/.copilot/session-state/<session-id>/ (or $COPILOT_HOME/session-state/), one folder per session with events.jsonl and workspace files (session-state), plus the rebuildable session-store.db index | /session delete, /session delete-all or /session prune, or x twice in the sidebar. No age-based sweep is documented. |
| Cursor | Staff replies and reports put transcripts at ~/.cursor/projects/<encoded-path>/agent-transcripts/*.jsonl, with the sidebar list in Cursor's local state.vscdb index (#162757); the docs describe neither path | Developer: Delete Old Chats in the Command Palette (Cursor 3.25 and later), or deleting the files yourself. Archiving a chat does not delete it. No age-based sweep is documented. |
A copy is a backup only if the tool can read it again. Retention is not a backup; the delete page has the commands.
Claude Code#
Where the files are#
The transcript is the saved conversation file; the rest sits beside it. A session can have a sibling <session-id>/ folder — a sidecar — with subagents/, tool-results/, and mcp-tasks/. Copy it for subagent and large-tool-output detail; copy ~/.claude/file-history/<session-id>/ for rewind points (checkpoint snapshots). Auto memory is under ~/.claude/projects/<project>/memory/; it is Claude's notes, not the transcript, and the sweep leaves it alone. For auto memory, <project> is the repository key, shared across worktrees and subdirectories, and not necessarily the transcript key; find it with /memory, or copy the whole projects/ tree. The resume guide covers CLAUDE_CONFIG_DIR; cleanupPeriodDays sets the sweep's age limit, and the delete page lists what it removes, including the Desktop and Cowork limit, which applies only once a transcript is older than both it and cleanupPeriodDays.
Copy it at session end#
A hook runs a command at a lifecycle event. Claude Code's SessionEnd event fires when a session ends; it receives the transcript path as transcript_path, and the sessions docs name archiving as the use.
Add this to ~/.claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/hooks/backup-transcript.sh",
"timeout": 10
}
]
}
]
}
}The script reads the hook JSON from stdin and copies the transcript into a dated folder. It needs jq on PATH:
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
transcript="$(printf '%s' "$payload" | jq -r '.transcript_path // empty')"
[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0
dest="$HOME/claude-session-backups/$(date +%Y/%m/%d)"
mkdir -p "$dest"
cp -- "$transcript" "$dest/"Save it as ~/.claude/hooks/backup-transcript.sh and make it executable with chmod +x.
The default SessionEnd budget is 1.5 seconds. A per-hook timeout raises it, up to 60 seconds; CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS sets it explicitly. A large transcript needs more or the copy can be cancelled.
SessionEnd fires on a normal exit, /clear, and switching sessions with /resume; the matcher can filter those reasons. It also fires in the IDE extensions and desktop app. Cloud sessions do not read local settings, so the hook misses them.
A hook cannot run after a crash or a killed process. If power loss is likely, add a scheduled copy.
Copy it on a schedule#
With Claude Code closed, copy the whole project tree:
rsync -a "$HOME/.claude/projects/" /backup/claude/projects/Add ~/.claude/file-history/ for checkpoint snapshots and the project memory/ directories for auto memory. A copy while Claude Code is writing can catch a half-written last line; copy after exit.
Restore it#
Put the .jsonl back under the same project directory, or resume it from the backup path:
claude --resume /backup/claude/projects/<project>/<session-id>.jsonlclaude --resume <session-id> works from any directory when exactly one project holds that ID. Resuming by path avoids a project-key mismatch after restoring elsewhere. If you want the subagent and large-tool-output detail, keep the <session-id>/ folder beside the .jsonl; the main transcript resumes without it.
Auto memory does not travel in the transcript. Copy the repository's projects/<project>/memory/ directory, or put the fact in a handoff file. The resume guide covers the picker, names and what a resumed conversation restores.
Codex#
Where the files are#
Codex calls a transcript a rollout. CODEX_HOME (default ~/.codex) moves the store and configuration together, so a copy restored to the default home will not appear under a custom one; the troubleshooting page covers the directories. The Codex docs are inconsistent: advanced configuration calls history.jsonl a session transcript, while troubleshooting treats sessions/ as the transcript store and history.jsonl as prompt history. In practice, use sessions/ for transcripts; history.jsonl is prompt history — history.persistence = "none" turns it off, and history.max_bytes caps and prunes it. codex delete removes the transcript and descendant sessions; codex archive only hides the chat.
Copy it at session end#
Codex supports SessionEnd hooks in ~/.codex/hooks.json or inline in ~/.codex/config.toml. The input includes transcript_path, and the docs say the hook can read the transcript while it runs.
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.codex/hooks/backup_transcript.py",
"timeout": 3
}
]
}
]
}
}The script can use the Claude Code shape: read JSON from stdin, take transcript_path, copy it to a dated folder. SessionEnd defaults to one second and supports up to three, so a very large rollout may need a scheduled copy.
Codex runs it on a normal close, when you archive or delete an open conversation, or after a conversation has been idle and not open in any connected client for 30 minutes. It does not run for subagents. New hooks must be trusted with /hooks; SessionEnd is synchronous.
Copy it on a schedule#
With Codex closed, copy both directories:
codex_home="${CODEX_HOME:-$HOME/.codex}"
rsync -a "$codex_home/sessions/" /backup/codex/sessions/
rsync -a "$codex_home/archived_sessions/" /backup/codex/archived_sessions/Include history.jsonl if you want prompt history as well.
Restore it#
Copy the directories back into the Codex home with Codex closed. codex resume reads the local store; codex unarchive <session> returns an archived chat to the active list.
The docs describe no import command. The safest restore is the whole Codex home from the same backup. If one rollout does not appear, resume by session ID or restore the whole home. Otherwise, point CODEX_HOME at the restored home before starting Codex. The Codex resume guide covers the picker, --last --all, and the active-writer error.
Gemini CLI#
Where the files are#
Run gemini --version first; this describes v0.65.0 (20 October 2026). Each session is one JSONL file in the project's chats directory:
~/.gemini/tmp/<project-id>/chats/session-2026-10-27T09-15-0b2adf84.jsonl<project-id> is a short slug for the absolute directory where you started gemini, mapped in ~/.gemini/projects.json. Starting in a subdirectory or in a worktree (a second checkout of the same repository) gives a different ID and a different folder to copy. The file's first line is metadata (session UUID, project hash, start time); the rest is the conversation, tool calls and token counts.
Two other paths hold checkpoint data, not the transcript: ~/.gemini/tmp/<project-id>/checkpoints and the shadow Git repository at ~/.gemini/history/<project-id> — a private Git repo the CLI keeps for those snapshots. Copy them if you use /restore; the rewind guide explains what they can put back.
Copy it at session end#
Gemini CLI's SessionEnd hook fires when the CLI exits or a session is cleared, and the hook input includes transcript_path. Put this in ~/.gemini/settings.json:
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"name": "backup-transcript",
"type": "command",
"command": "$HOME/.gemini/hooks/backup-transcript.sh",
"timeout": 10000
}
]
}
]
}
}The script reads the hook JSON from stdin and copies the transcript into a dated folder. It needs jq on PATH, and it must print nothing to stdout: a hook's stdout is parsed as JSON, and a stray line breaks the parse.
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
transcript="$(printf '%s' "$payload" | jq -r '.transcript_path // empty')"
[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0
dest="$HOME/gemini-session-backups/$(date +%Y/%m/%d)"
mkdir -p "$dest"
cp -- "$transcript" "$dest/"Save it as ~/.gemini/hooks/backup-transcript.sh and make it executable with chmod +x. The timeout is in milliseconds, so 10000 is ten seconds.
The docs call SessionEnd best effort: the CLI does not wait for the hook, and flow-control fields are ignored. A small copy usually lands; a large transcript or a slow disk may not. The scheduled copy is the one to rely on.
Copy it on a schedule#
With Gemini CLI closed, copy every project's chats folder and the registry that maps folder paths to project IDs:
dest="$HOME/gemini-session-backups/$(date +%Y-%m-%dT%H-%M-%S)"
mkdir -p "$dest"
cp -a "$HOME/.gemini/projects.json" "$dest/"
for d in "$HOME"/.gemini/tmp/*/; do
id="$(basename "$d")"
[ -d "$d/chats" ] || continue
mkdir -p "$dest/$id"
rsync -a "$d/chats/" "$dest/$id/chats/"
doneKeep projects.json with the copy: it is what maps a folder path to the project ID. Add ~/.gemini/history/ and each project's checkpoints/ directory if you use checkpointing.
Restore it#
Copy the project's chats/ folder back under the same project ID, with Gemini closed, then check gemini --list-sessions and resume. The ID comes from the absolute path, so a restored folder under a different path is not found. Two ways around that:
- Copy the
.jsonlinto the new project's chats folder. The loader does not check the recorded project path, though the metadata still names the old project. - Import it with
gemini --session-file /backup/session-....jsonl. It loads the transcript, gives it a new session ID and writes it into the current project's chats folder. The flag is ingemini --helpat v0.65.0 but not in the flag table.
The resume guide covers project IDs, the picker, and the cleanup path that can remove a resumed file.
Qwen Code#
Where the files are#
Run qwen --version first. Each recorded session is one JSONL file in the project's chats directory (session data):
~/.qwen/projects/<sanitized-cwd>/chats/<session-id>.jsonl<sanitized-cwd> is the absolute project path with every character that is not a letter or digit replaced by -, lowercased on Windows (source). QWEN_HOME moves the configuration tree, and QWEN_RUNTIME_DIR moves runtime output, including projects/; a runtime directory set for one run puts that session somewhere else. Ask the CLI which paths it is using instead of guessing:
qwen sessions list --json --limit 50Each JSON line includes filePath and cwd, the values the CLI actually resolved for this project (commands).
Two other stores matter. A <session-id>.runtime.json file in the same chats/ folder is live-session status, not history. ~/.qwen/file-history/<session-id>/ holds the checkpoint backups /rewind uses; copy it if you rely on rewind. general.cleanupPeriodDays (30 days by default) deletes file-history backups and debug logs, not the transcripts (settings).
Copy it at session end#
Qwen Code's SessionEnd hook receives transcript_path, the same field Claude Code's hook uses, so the copy script is the same. Put this in ~/.qwen/settings.json (hooks):
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "$HOME/.qwen/hooks/backup-transcript.sh",
"timeout": 10
}
]
}
]
}
}The script reads the hook JSON from stdin and copies the transcript into a dated folder. It needs jq on PATH:
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
transcript="$(printf '%s' "$payload" | jq -r '.transcript_path // empty')"
[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0
dest="$HOME/qwen-session-backups/$(date +%Y/%m/%d)"
mkdir -p "$dest"
cp -- "$transcript" "$dest/"Save it as ~/.qwen/hooks/backup-transcript.sh and make it executable with chmod +x. The timeout is in seconds and defaults to 60. The hook fires when a session ends, including a normal exit and /clear; the payload's reason says which. It cannot run after a crash or a killed process, so add the scheduled copy if that matters.
Copy it on a schedule#
With Qwen closed, copy the project tree and the rewind backups:
rsync -a "$HOME/.qwen/projects/" /backup/qwen/projects/
rsync -a "$HOME/.qwen/file-history/" /backup/qwen/file-history/If QWEN_RUNTIME_DIR is set, use "$QWEN_RUNTIME_DIR/projects/" for the first line. If QWEN_HOME is set, projects/ and file-history/ are under it unless QWEN_RUNTIME_DIR overrides the projects tree. Copying while Qwen is writing can catch a half-written last line; copy after exit.
Restore it#
Put the .jsonl back under the same <sanitized-cwd>/chats/ directory with Qwen closed, then run qwen --resume <session-id> or qwen --resume from that project directory. The lookup is scoped to the current project, and the transcript's first record has to name the same working directory, so restoring the file under a different absolute path does not make the session appear. Renaming or moving a project makes its saved sessions unreachable from the new path; that is an open report (#12553, open, 23 September 2026). Keep the original path, or restore the whole project at it.
There is no documented import for an /export file or for a transcript from another machine, so the route is to move projects/ to the same path on the other machine. qwen sessions list --json confirms the file is visible before you try to resume.
OpenCode#
Where the files are#
OpenCode uses a SQLite database; the V2 CLI prints its path with opencode debug paths db, the V1 CLI with opencode db path, and OPENCODE_DB can move it. V2 normally runs a shared background server that owns sessions, so another server's client sees that server's database. A session is a row set, not a file; the portable copy is an export. opencode session delete removes a session and, in V2, its child sessions. The V1 CLI's OPENCODE_DISABLE_PRUNE says "Disable pruning of old data" but not what or when, and V2 does not list it; treat cleanup as undocumented rather than absent.
Copy it on a schedule#
For a whole-store backup, use SQLite's backup command, not a file copy:
#!/usr/bin/env bash
set -euo pipefail
# V2
db="$(opencode debug paths db)"
# V1: use this line instead of the one above
# db="$(opencode db path)"
dest="$HOME/opencode-backups/$(date +%Y-%m-%dT%H-%M-%S).db"
mkdir -p "$(dirname "$dest")"
sqlite3 "$db" ".backup '$dest'"Run it from cron, launchd, or systemd. The first lookup is V2; on V1, use the commented opencode db path line instead. SQLite's backup API handles a live database and its write-ahead log; a plain cp can miss the WAL and corrupt it.
For one session, export it:
opencode session export ses_abc123 > session.jsonBoth V1 and V2 accept --sanitize to redact sensitive transcript and file data; V1 uses opencode export [sessionID], V2 uses opencode session export <id>. The JSON is importable, so it is a single-session backup, not just a reading copy.
A V2 plugin can subscribe to session.idle (when the session stops working) and session.deleted (when it is deleted) to export automatically. session.idle can fire more than once, so this is not a guaranteed end-of-process backup; keep the scheduled copy for that. The event names are documented, but we did not run a plugin.
Restore it#
Import the JSON into V2 with the V2 import command:
opencode session import session.json --directory ~/code/projectV1 uses opencode import session.json. Check opencode session list; the docs do not say whether the ID is preserved.
For a whole-store restore, stop the service, keep the current database, put the backup at the database path (V2: opencode debug paths db; V1: opencode db path), and start the service. That is a file-level restore, not a documented merge: anything created after the backup is gone. The docs warn against editing the database while troubleshooting, so treat this as a last resort behind export/import. On a remote server, restore its database there. The OpenCode resume guide covers projects, imports and the database.
V1's uninstall removes session data and configuration unless you pass --keep-data and --keep-config; V2's uninstall keeps session data, configuration and state. Back up before either.
GitHub Copilot CLI#
Where the files are#
The layout below is current for 1.0.104 (27 October 2026); run copilot --version if the install is older. A local Copilot CLI session is a folder under the session-state directory: ~/.copilot/session-state/<session-id>/, or $COPILOT_HOME/session-state/<session-id>/ when you have moved the home. Each folder holds events.jsonl, the event log, and the workspace artifacts the docs list as plans, checkpoints and tracked files. ~/.copilot/session-store.db is a SQLite index over those folders for checkpoint lookup and search; it is rebuildable, so it is the session folders that matter.
COPILOT_HOME replaces the whole ~/.copilot tree, so a copy restored into a different home is not found until you point the variable at it. Separately, Copilot CLI syncs session history to your GitHub account by default (remoteExport); --sync-session=off turns syncing off for one run. That synced copy is not in your backup.
Copy it at session end#
Copilot CLI hooks receive different fields from Claude Code's and Codex's. A sessionEnd hook gets sessionId, cwd, timestamp and reason — no transcript path — so the script builds the folder path from the ID and COPILOT_HOME.
Put this in ~/.copilot/hooks/backup-session.json, or under $COPILOT_HOME/hooks/:
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"type": "command",
"bash": "$HOME/.copilot/hooks/backup-session.sh",
"powershell": "$HOME/.copilot/hooks/backup-session.ps1",
"timeoutSec": 30
}
]
}
}The shell script reads the hook JSON from stdin, takes the ID, and copies that one folder into a dated directory, keeping the session ID as the folder name so a second end for the same session updates the copy instead of nesting a folder inside it:
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
id="$(printf '%s' "$payload" | jq -r '.sessionId // empty')"
[ -n "$id" ] || exit 0
root="${COPILOT_HOME:-$HOME/.copilot}"
src="$root/session-state/$id"
[ -d "$src" ] || exit 0
dest="$HOME/copilot-session-backups/$(date +%Y/%m/%d)/$id"
mkdir -p "$dest"
cp -a -- "$src/." "$dest/"Save it as ~/.copilot/hooks/backup-session.sh and make it executable with chmod +x. It needs jq on PATH. Write the PowerShell equivalent for the powershell key if you use Windows; a hook with only a bash command has nothing to run there.
The default timeout is 30 seconds, and timeoutSec raises it. Hook files are read when the CLI starts, so restart after adding or editing one. sessionEnd fires when the session terminates, with a reason of complete, error, abort, timeout or user_exit; /clear is the user_exit case, and it closes the old session while the CLI keeps running. That case dispatches the hook in the background, so a copy that takes a few seconds still finishes. Both hook paths above sit inside the Copilot home; change them if COPILOT_HOME is set.
A hook cannot run after a crash or a killed process. If that matters, add the scheduled copy.
If the sandbox is enabled — the restricted area where Copilot CLI runs agent commands — user and repository hooks run inside it with the same access as the agent's shell commands, and naming a path in cwd or env does not widen that access. The copy to $HOME/copilot-session-backups then fails until you add that directory to sandbox.userPolicy, or until you use the scheduled copy below, which runs outside the CLI. Policy hooks always run on the host, outside the sandbox, and administrators install those.
Copy it on a schedule#
With Copilot CLI closed, copy the session folders:
copilot_home="${COPILOT_HOME:-$HOME/.copilot}"
rsync -a "$copilot_home/session-state/" /backup/copilot/session-state/Copy session-store.db too if you want the index without a rebuild. Because it is SQLite, copy it with the CLI closed, or accept that a live copy can catch a write; losing it costs a reindex, not the transcript.
Restore it#
Put the session folder back under session-state/ with the CLI closed, start the CLI, and run /chronicle reindex. The docs describe reindex as the step for session files moved to another machine or restored from a backup without the store; it rebuilds the index from the folders. Reindexing also syncs session data to your account. If the restored sessions should stay local, set remoteExport to false or start the CLI with --sync-session=off before reindexing; otherwise expect the restored sessions to appear in your GitHub account. Then copilot --resume=<session-id>; the Copilot CLI session guide covers the picker and what a resumed session restores. Copying the whole ~/.copilot and pointing COPILOT_HOME at it is the cleaner move when you have the whole home.
Before deciding a transcript is lost, update the CLI. A strict schema check rejected older compaction events in 1.0.35-3; the issue was closed on 23 April 2026 with a reply saying 1.0.35-4 fixed it (#2899, closed). 1.0.86 (17 September 2026) added resuming sessions whose transcript files contain recoverable corruption. VS Code Copilot Chat still does not open a CLI session folder as a chat (#2000, open, 12 March 2026), so restore into the CLI.
Cursor#
Where the files are#
Run Cursor → About Cursor. 3.27 was the newest editor build on 27 October 2026. Cursor's documentation does not say where local chats are stored; the paths below come from Cursor staff replies and reports, so check them on your build.
A chat's transcript is a JSONL file, one JSON object per line, under ~/.cursor/projects/<encoded-path>/agent-transcripts/. On Windows the root is %USERPROFILE%\.cursor\projects. <encoded-path> is usually the project path with separators turned into dashes. The sidebar list is a separate index in state.vscdb under Cursor's User storage (globalStorage and per-workspace workspaceStorage) — on macOS ~/Library/Application Support/Cursor/User, on Windows %APPDATA%\Cursor\User, on Linux ~/.config/Cursor/User. A staff reply in #162757 (10 June 2026) says there is no built-in way to rebuild the sidebar from the transcript files, and a later reply in the same thread warns against editing state.vscdb because it is shared across windows and its schema changes.
The editor and the CLI keep separate histories. A staff reply in #159779 (5 May 2026, Cursor 3.2.21) says copying transcript files does not bridge the index; a second in #161787 (28 May 2026) says the CLI and desktop app do not share chats. If you use both, include ~/.cursor in the backup.
Copy it at session end#
Cursor's hooks reference documents a sessionEnd event, and the common hook payload includes transcript_path (null if transcripts are disabled). Put this in ~/.cursor/hooks.json:
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"command": "./hooks/backup-transcript.sh"
}
]
}
}User hooks run from ~/.cursor/, so ./hooks/backup-transcript.sh is ~/.cursor/hooks/backup-transcript.sh. The script reads the payload and copies the file. It needs jq on PATH:
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
transcript="$(printf '%s' "$payload" | jq -r '.transcript_path // empty')"
[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0
dest="$HOME/cursor-session-backups/$(date +%Y/%m/%d)"
mkdir -p "$dest"
cp -- "$transcript" "$dest/"Save it and make it executable with chmod +x. Cursor describes sessionEnd as fire-and-forget: the response is logged but not used. The hook copies the file named in transcript_path; the scheduled copy of projects/ catches subagent transcripts and anything stored beside it. Treat the hook copy as a bonus and keep the scheduled copy as the main backup.
Copy it on a schedule#
With Cursor and any CLI session closed, copy the transcript tree and the User folder:
rsync -a "$HOME/.cursor/projects/" /backup/cursor/projects/Then copy Cursor's User folder, or at least globalStorage and workspaceStorage, from the path above. The index is what makes a chat appear in the sidebar; the transcripts are the part no rebuild can recreate. Copy both.
Restore it#
Put projects/ and the User folder back with Cursor closed, then open the exact workspace: a saved .code-workspace, the same folder, and the same SSH or WSL route. Cursor keys history to the workspace identity, not just the folder path (#175609, 21 October 2026, Cursor 3.25.9). If only the transcript is restored, it is still readable as a file, but the sidebar may not list it.
The Cursor chat history guide covers the workspace identity, the Agents Window, and the CLI's separate history. Cursor's docs describe no import for a transcript or an export.
Another machine, or a phone#
None of the seven documents syncing the local session store between machines; each has its own continuation surface (below), and copying the store is a file operation you own.
| Tool | Move a session | Continue from a browser or phone |
|---|---|---|
| Claude Code | Copy the .jsonl and sidecars, then resume by path or from the matching project folder by ID | Remote Control for a session on your machine; cloud sessions for Anthropic-hosted work |
| Codex | Copy sessions/ and archived_sessions/ into the other Codex home and resume there | Codex Cloud from ChatGPT on the web, mobile, or desktop |
| Gemini CLI | Copy the project's chats/ folder into the same project ID, or import the .jsonl with gemini --session-file | No documented continuation surface; --session-file is the way to carry a transcript to another machine |
| Qwen Code | Copy projects/ to the same absolute project path on the other machine; the recorded working directory is part of the lookup | No hosted continuation; a Web Shell served by qwen serve on the machine that holds the store |
| OpenCode | Export the session as JSON and import it, or restore the database on the server that owns it | V2 web UI served by opencode serve, paired with opencode pair; a remote server with --server |
| GitHub Copilot CLI | Copy session-state/ into the other Copilot home and run /chronicle reindex, or point COPILOT_HOME at the restored home | Remote control with /remote on; synced sessions are view-only on GitHub.com and GitHub Mobile |
| Cursor | Copy projects/ and the User folder and keep the same workspace identity, or copy the whole ~/.cursor for both surfaces, knowing it also carries rules, MCP configuration, permissions and hooks | No documented sync between machines; cloud agents have their own transcript view |
Remote Control keeps execution and files on your machine while the app drives the session; the machine must stay on. Cloud sessions can be pulled into a terminal with --teleport, and the mobile guide explains the Code tab. Codex cloud tasks are separate from the local Codex home. opencode pair links and QR codes expire after five minutes; a remote client sees that server's database, so back it up there.
Copilot CLI remote control is off by default. /remote on prints a link for GitHub.com or GitHub Mobile, and /remote followed by Ctrl+O shows a QR code. The session keeps running on your machine, so use /keep-alive on to stop it sleeping. Session syncing is the other, separate copy: synced sessions are view-only on GitHub.com and GitHub Mobile.
Sync the backup destination, not the live store: two writers on one SQLite database, or two appending to one JSONL file, can corrupt it. A cloud sync folder is another unencrypted copy; encrypt it.
Tools that capture sessions for you#
Beacon, by Asymptote Labs, captures prompts, tool calls, edits and outcomes from Claude Code, Codex, OpenCode and other harnesses into a local dashboard or the hosted Beacon Cloud. Local use does not send your agent activity to Asymptote. Cloud has two modes: Standard sends prompts, responses, tool activity, command output, diffs and file contents as captured, with common secret formats masked on the device; Metadata only removes that content before upload but still sends paths, exit codes, token counts and full command lines.
The privacy terms matter more than the capture list. Beacon's privacy policy, effective 15 October 2026, says research and model training are on by default with an opt-out, Cloud content is kept while the account is active, deletion requests are processed within 30 days, and deleted data remains in backups for up to 35 days. Its redaction docs call the masking pattern-based and say an unlabeled credential of no recognizable shape is not detected. We have not run Beacon; these statements are from its own pages, checked 27 October 2026.
A local index is a different trade. deja reads the session stores already on the machine and builds a search index; its security model says the index stays local, but it does not survive a disk loss. We have not run it. Claude Code History Viewer, covered in the export guide, is another local-first viewer.
Before trusting a capture tool, check which mode is on, what leaves the machine, where the copy lands, who can read it, and how you export or delete it. Then check the restore; a dashboard you cannot export from is not a backup.