Documentation
¶
Overview ¶
Package bundle creates and (later) reads .codexbundle archives: a ZIP containing a manifest, a checksum map, and the copied Codex rollout files.
A .codexbundle is a portable, hosting-free way to move local Codex sessions between devices. It never includes or modifies Codex's SQLite state DB.
Index ¶
- Constants
- func DirExists(p string) bool
- func ParseSince(s string) (time.Time, error)
- func StripImagesJSONL(plain []byte) (out []byte, count int)
- type Action
- type CWDInfo
- type CWDMapping
- type CWDSummary
- type Checksums
- type ExportOptions
- type ExportResult
- type ImportItem
- type ImportOptions
- type ImportResult
- type InspectResult
- type Manifest
- type ManifestSession
- type SecretScanResult
- type TranslateItem
- type TranslateOptions
- type TranslateResult
Constants ¶
const ( // MaxSessionBytes caps a single session entry's uncompressed size. MaxSessionBytes = 100 << 20 // 100 MiB // MaxMetadataBytes caps manifest.json / checksums.json uncompressed size. MaxMetadataBytes = 16 << 20 // 16 MiB // MaxBundleEntries caps the number of files in a bundle. MaxBundleEntries = 100_000 // MaxBundleUncompressed caps the total uncompressed size across all entries. MaxBundleUncompressed = 2 << 30 // 2 GiB )
Bundle resource limits, enforced before reading or writing untrusted bundle content. They bound the damage a malicious bundle can do via decompression bombs or oversized entries (memory, CPU, and disk exhaustion). See the "Resource limits" findings in docs/security/audit.md (SEC-2/SEC-3).
const ( ManifestName = "manifest.json" ChecksumsName = "checksums.json" )
Bundle file names.
const FormatVersion = "codex-sync-bundle-v1"
FormatVersion identifies the bundle layout. Bump this on breaking changes.
Variables ¶
This section is empty.
Functions ¶
func DirExists ¶
DirExists reports whether p exists on the local machine and is a directory. It only reads (os.Stat); it never creates anything.
The recorded cwd comes from an untrusted bundle, and os.Stat on a UNC path (\\server\share or //server/share) or a Windows device path triggers outbound SMB / name resolution — which can leak NetNTLM credentials to an attacker-chosen host just from inspecting a bundle. Such remote-looking paths are therefore reported as not-present without ever being statted. See SEC-11 / Finding 3 in docs/security/audit.md.
func ParseSince ¶ added in v0.4.0
ParseSince accepts either an absolute date (YYYY-MM-DD, interpreted as UTC midnight) or a relative duration ending in d/h/m (e.g. 7d, 48h, 90m) measured back from now. It returns the cutoff instant; sessions updated at or after it pass the filter. It is shared by the CLI (`export --since`) and the desktop UI.
func StripImagesJSONL ¶ added in v0.5.0
StripImagesJSONL removes inline base64 image payloads from a rollout/transcript JSONL buffer, returning the rewritten bytes and the number of images stripped.
It is lossy and strictly opt-in (export --strip-images): the conversation text is kept, but embedded picture bytes are replaced with a short placeholder so an image-heavy bundle becomes much smaller. It recognizes the two shapes that appear in Codex and Claude Code transcripts:
- data URIs: "data:image/png;base64,<payload>" (the OpenAI/Codex shape) — the "data:...;base64," prefix is kept and the payload replaced.
- base64 source objects: {"type":"base64","data":"<payload>", ...} (the Anthropic shape) — the "data" field is replaced.
Lines and any content without an image are preserved byte-for-byte; only the objects on the path to a stripped image are re-encoded, so the file otherwise stays exactly as it was.
Types ¶
type Action ¶
type Action string
Action describes what import decided to do with one bundle entry.
const ( ActionImport Action = "import" // new file; copied (unless dry-run) ActionSkipIdentical Action = "skip-identical" // target exists with same checksum ActionConflict Action = "conflict" // target exists with different checksum; not overwritten ActionReplace Action = "replace" // conflict, overwritten after backing up the local file ActionImportCopy Action = "import-copy" // conflict, imported as a brand-new session (fresh id + filename) ActionUpdate Action = "update" // --merge: local is a prefix of the bundle; extended in place (append-only, lossless) ActionSkipAhead Action = "skip-ahead" // --merge: bundle is a prefix of the local file; local already current ActionSkipArchived Action = "skip-archived" // archived_sessions entry; not imported in v0.1 ActionSkipNonSession Action = "skip-non-session" // unexpected non-session file ActionSkipDeselected Action = "skip-deselected" // not among the --session ids requested for import )
type CWDInfo ¶
type CWDInfo struct {
// Path is the first-seen original spelling of the cwd (for display).
Path string
// Count is how many sessions recorded this cwd.
Count int
// ExistsLocal reports whether the directory exists on this machine. A
// session whose cwd does not exist locally is hidden in Codex's project
// sidebar until the folder is created or the cwd is remapped on import.
ExistsLocal bool
}
CWDInfo describes one distinct recorded working directory across a bundle's sessions and whether that directory exists on the local machine.
type CWDMapping ¶
CWDMapping rewrites a session's recorded working directory from Old to New during import. It is the only feature that mutates session content, and it touches nothing except the canonical "cwd" field of the session_meta line.
func ParseCWDMappings ¶
func ParseCWDMappings(specs []string) ([]CWDMapping, error)
ParseCWDMappings parses and validates a list of "OLD=NEW" mapping specs.
Rules:
- syntax must be OLD=NEW (split on the first '='),
- OLD must not be empty,
- NEW must not be empty and must be absolute/plausible for the target OS,
- OLD and NEW must differ after normalization,
- duplicate OLD paths (case-insensitive on Windows) are rejected.
type CWDSummary ¶
type CWDSummary struct {
// Dirs are the distinct non-empty recorded cwds, sorted by path.
Dirs []CWDInfo
// UnknownCWD counts sessions with no recorded cwd (e.g. compressed
// sessions, whose cwd is unknown in v0.1).
UnknownCWD int
// MissingCount is how many entries in Dirs do not exist locally.
MissingCount int
}
CWDSummary groups a bundle's sessions by their recorded working directory and reports which directories are missing on the local machine. It is the data behind the "project folders" view that helps users find the #1 portability gotcha (imported sessions hidden because their cwd does not exist here).
func SummarizeCWDs ¶
func SummarizeCWDs(sessions []ManifestSession, exists func(string) bool) CWDSummary
SummarizeCWDs groups sessions by recorded cwd and, for each distinct directory, reports the session count and whether it exists locally. Grouping is case-insensitive on Windows (via normalizeCWD), matching how cwd is compared elsewhere; the first-seen spelling is preserved for display. The exists function is injected so the logic is testable without touching the filesystem; production callers pass DirExists.
type Checksums ¶
Checksums maps each bundle-relative file path (forward slashes) to its SHA-256 hex digest. It is written as checksums.json at the root of the ZIP and covers every file in the bundle except checksums.json itself.
type ExportOptions ¶
type ExportOptions struct {
// Tool selects which agent's sessions to export ("" / agent.Codex scans the
// Codex home; agent.Claude scans ClaudeHome). The chosen tool is recorded in
// the manifest so import knows how to place the files back.
Tool agent.Kind
// ClaudeHome is the resolved Claude Code home, used only when Tool is
// agent.Claude. (The Codex home is passed to Export directly.)
ClaudeHome claudehome.Home
// ProjectPath, when non-empty, restricts the export to sessions whose
// SessionMeta cwd matches this (already absolute) path. Leave empty to
// export every session regardless of cwd (the `--all` behavior).
ProjectPath string
// OutputPath is the .codexbundle file to write.
OutputPath string
// IncludeArchived also considers archived sessions.
IncludeArchived bool
// Since, when non-zero, restricts the export to sessions whose file
// modification time is at or after this instant.
Since time.Time
// SessionID, when non-empty, exports exactly the single session whose
// thread id equals (or uniquely begins with) this value, regardless of
// cwd. It is mutually exclusive with project/all filtering.
SessionID string
// OnlyThreadIDs, when non-empty, selects exactly the sessions whose thread id
// is in this set (exact match), bypassing project/all/since/single selection.
// It is used by LAN sync to bundle precisely the sessions a peer is missing.
OnlyThreadIDs []string
// Match, when non-empty, additionally keeps only sessions whose conversation
// text matches this query (composes with the project/all/since filters). Regex
// and case-sensitivity follow the two flags below.
Match string
MatchRegex bool
MatchCaseSensitive bool
// WithGit forces capture of the project's git metadata (remote, branch,
// commit, dirty/unpushed) into the manifest even when ProjectPath is empty
// (e.g. with --all or --session). When ProjectPath is set, git metadata is
// always captured regardless of this flag.
WithGit bool
// StripImages replaces inline base64 image payloads in each session with a
// short placeholder before adding it to the bundle. It is lossy (the picture
// bytes are dropped) and opt-in, used to shrink image-heavy bundles. The
// conversation text is preserved; compressed .jsonl.zst sessions are stripped
// too when the zstd tool is available (otherwise copied as-is with a warning).
StripImages bool
// Redact replaces likely secrets (API keys, tokens, private keys) in each
// session with typed placeholders before bundling. Lossy and opt-in, for
// sharing/syncing a session without leaking credentials.
Redact bool
}
ExportOptions configures an export.
type ExportResult ¶
type ExportResult struct {
BundlePath string
Manifest Manifest
IncludedCount int
TotalScanned int
CompressedSkipped int // compressed sessions skipped by cwd filter (cwd unknown)
ImagesStripped int // images replaced when StripImages is set
BytesSaved int64 // bundle bytes saved by stripping (original minus stored)
SecretsRedacted int // secrets replaced when Redact is set
Warnings []string
}
ExportResult summarizes what was exported.
func Export ¶
func Export(home codexhome.Home, opts ExportOptions) (ExportResult, error)
Export scans the Codex home, selects sessions (optionally filtered to a project's cwd), and writes a .codexbundle ZIP containing the rollout files, a manifest, and a checksum map. It never reads or writes Codex's SQLite.
type ImportItem ¶
type ImportItem struct {
BundlePath string
DestPath string
Action Action
OriginalCWD string
CWDMismatch bool
// Mapped reports whether --map-cwd rewrote this session's cwd.
Mapped bool
// Copied reports whether this entry was imported as a brand-new session
// (ActionImportCopy, --import-as-copy) with NewThreadID as its fresh id.
Copied bool
// NewThreadID is the freshly assigned session id when Copied is true.
NewThreadID string
// BackupPath, when non-empty, is where the pre-existing local file was
// backed up before being replaced (ActionReplace, --replace-with-backup).
BackupPath string
// LinesAdded is the number of new lines a --merge update appended on top of
// the existing local file (ActionUpdate). It is an approximate, line-based
// count for reporting only.
LinesAdded int
// contains filtered or unexported fields
}
ImportItem is the plan/outcome for a single bundle entry.
type ImportOptions ¶
type ImportOptions struct {
BundlePath string
DryRun bool
// ProjectPath, when non-empty, enables per-session cwd-mismatch checks
// against this (already absolute) path.
ProjectPath string
// MapCWD, when set, rewrites matching sessions' recorded cwd on import.
// Only plain .jsonl files are rewritten; .jsonl.zst files that match a
// mapping are copied byte-for-byte and reported as unmappable.
MapCWD []CWDMapping
// MapCWDHere is the convenience form of MapCWD for the common "put these
// sessions under the project I'm in right now" case: it maps the bundle's
// single recorded project cwd to HereDir without the caller having to look up
// the old path. It requires the bundle to contain exactly one distinct source
// cwd; a bundle spanning multiple projects is ambiguous and rejected (use
// MapCWD explicitly). Mutually exclusive with MapCWD.
MapCWDHere bool
// HereDir is the absolute destination path for MapCWDHere (the caller's
// current working directory). Ignored unless MapCWDHere is set.
HereDir string
// ReplaceWithBackup turns conflicts (a local file with different content
// for the same session) into a replace: the local file is backed up next
// to itself and then overwritten with the bundle's version. Without it,
// conflicts are reported and skipped (the default, never-overwrite behavior).
ReplaceWithBackup bool
// SessionIDs, when non-empty, restricts the import to the bundle sessions
// whose thread id equals (or uniquely begins with) one of these values; every
// other session in the bundle is skipped (ActionSkipDeselected). An id that
// matches no session in the bundle is an error (nothing is written).
SessionIDs []string
// ImportAsCopy turns conflicts into a brand-new session: the bundle's
// version is assigned a fresh session id and written under a new rollout
// filename, so it coexists with the diverged local session rather than being
// skipped or replacing it. Only plain .jsonl files can be copied; a
// compressed (.jsonl.zst) conflict, or one without a session_meta id, stays a
// skipped conflict. Mutually exclusive with ReplaceWithBackup at the CLI.
ImportAsCopy bool
// Merge enables append-only incremental sync. Session files are append-only
// logs, so when a session that already exists locally grew on the other
// device, the bundle's copy is a byte-prefix superset of the local file.
// With Merge set, such a "grown" session is updated in place (ActionUpdate):
// the longer bundle version is written, which appends the new lines without
// losing anything. When the local file is already a superset of the bundle's
// (the local copy is ahead), the session is left untouched (ActionSkipAhead).
// Sessions that genuinely diverged (neither side is a prefix of the other)
// remain conflicts, handled by ReplaceWithBackup/ImportAsCopy or skipped.
// Merge composes with those two flags: it resolves clean growth first and
// hands true divergence to them.
Merge bool
}
ImportOptions configures an import.
type ImportResult ¶
type ImportResult struct {
Manifest Manifest
Items []ImportItem
Imported int
SkippedIdentical int
Conflicts int
SkippedOther int
// SkippedDeselected counts bundle sessions excluded by a --session filter.
SkippedDeselected int
CWDMismatchCount int
// Replaced counts conflicting sessions that were overwritten after the local
// file was backed up (--replace-with-backup).
Replaced int
// Updated counts sessions that grew on the other device and were extended in
// place by --merge (ActionUpdate). LinesAdded is the total number of lines
// those updates appended.
Updated int
LinesAdded int
// AlreadyAhead counts sessions left untouched by --merge because the local
// copy already contained everything in the bundle (and possibly more).
AlreadyAhead int
// ImportedCopies counts conflicting sessions imported as brand-new sessions
// (--import-as-copy).
ImportedCopies int
// Mapped counts imported sessions whose cwd was rewritten by --map-cwd.
Mapped int
// MappedCompressedSkipped counts compressed sessions that matched a
// mapping but could not be rewritten in v0.1 (copied byte-for-byte).
MappedCompressedSkipped int
ProjectProvided bool
DryRun bool
Warnings []string
}
ImportResult summarizes an import.
func Import ¶
func Import(home codexhome.Home, opts ImportOptions) (ImportResult, error)
Import validates a bundle end-to-end and, unless DryRun is set, copies its session files into the agent home (home.Root), preserving each agent's layout: Codex rollouts under sessions/YYYY/MM/DD/ and Claude Code transcripts under projects/<encoded-cwd>/. The bundle's recorded tool selects which.
Safety guarantees:
- The whole bundle's checksums are verified BEFORE anything is written.
- Unsafe entry paths (absolute, drive-letter, zip-slip) abort the import.
- Only the agent's recognized session files are imported (anything else is skipped and reported).
- Existing files are never overwritten: identical files are skipped, differing files are reported as conflicts and skipped.
- Writes are atomic (temp file + rename). The agent's index (Codex SQLite, Claude ~/.claude.json) is never touched.
- .jsonl.zst files are copied byte-for-byte; never parsed or decompressed.
type InspectResult ¶
InspectResult is the read-only view of a bundle: its manifest, its checksum map, and the names of the files it contains. Inspecting never extracts or writes anything.
func Inspect ¶
func Inspect(bundlePath string) (InspectResult, error)
Inspect opens a .codexbundle and reads manifest.json and checksums.json without extracting any session files. It validates that the bundle has the expected format version.
type Manifest ¶
type Manifest struct {
FormatVersion string `json:"format_version"`
// Tool records which coding agent the sessions came from ("codex" or
// "claude"). It is omitted for legacy Codex bundles, which are treated as
// "codex" on read, so older bundles remain importable unchanged.
Tool string `json:"tool,omitempty"`
CreatedAt string `json:"created_at"`
CreatedByDevice string `json:"created_by_device,omitempty"`
SourceOS string `json:"source_os"`
SourceCodexHome string `json:"source_codex_home"`
SourceProjectPath string `json:"source_project_path,omitempty"`
Git *git.Info `json:"git,omitempty"`
CodexVersion string `json:"codex_version,omitempty"`
Sessions []ManifestSession `json:"sessions"`
}
Manifest describes a bundle and everything inside it. It is written as manifest.json at the root of the ZIP.
type ManifestSession ¶
type ManifestSession struct {
ThreadID string `json:"thread_id,omitempty"`
OriginalPath string `json:"original_path"`
BundlePath string `json:"bundle_path"`
OriginalCWD string `json:"original_cwd,omitempty"`
Preview string `json:"preview,omitempty"`
FirstUserMessage string `json:"first_user_message,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Source string `json:"source,omitempty"`
ModelProvider string `json:"model_provider,omitempty"`
GitBranch string `json:"git_branch,omitempty"`
GitSHA string `json:"git_sha,omitempty"`
Archived bool `json:"archived"`
Compressed bool `json:"compressed"`
SizeBytes int64 `json:"size_bytes"`
SHA256 string `json:"sha256"`
}
ManifestSession is one rollout file recorded in the manifest.
type SecretScanResult ¶ added in v0.9.0
type SecretScanResult struct {
SessionsWithSecrets int // number of session entries containing >=1 finding
TotalFindings int // total secret findings across all sessions
}
SecretScanResult summarizes a scan of a bundle's session contents.
func ScanBundleSecrets ¶ added in v0.9.0
func ScanBundleSecrets(bundlePath string) (SecretScanResult, error)
ScanBundleSecrets opens a (plaintext) bundle and scans each session entry for likely secrets, so the pre-egress gate can refuse to export/sync credentials unless the user opts in. It scans the exact bytes that would leave the machine. Compressed entries are decompressed when zstd is available and skipped (counted as no findings) when it is not.
func (SecretScanResult) Any ¶ added in v0.9.0
func (r SecretScanResult) Any() bool
Any reports whether the bundle contains any likely secret.
type TranslateItem ¶ added in v0.3.0
type TranslateItem struct {
BundlePath string
DestPath string
SessionID string // id assigned in the target agent
Turns int
Action string // "translate" | "skip-existing" | "skip"
Reason string // why it was skipped, when Action == "skip"
}
TranslateItem is the plan/outcome for one translated session.
type TranslateOptions ¶ added in v0.3.0
TranslateOptions configures a cross-agent handoff import: reading a bundle of one agent's sessions and writing translated, continue-from-here sessions into a different agent's home.
type TranslateResult ¶ added in v0.3.0
type TranslateResult struct {
SourceTool agent.Kind
TargetTool agent.Kind
Translated int
SkippedExisting int
Skipped int
Items []TranslateItem
Warnings []string
DryRun bool
}
TranslateResult summarizes a cross-agent handoff import.
func TranslateImport ¶ added in v0.3.0
func TranslateImport(targetHome codexhome.Home, opts TranslateOptions) (TranslateResult, error)
TranslateImport reads the bundle (whatever agent it came from), converts each session into TargetTool's format via the handoff package, and writes the results into targetHome (whose Root is the target agent's home). It never overwrites: a session that already exists is skipped (translation is deterministic, so a re-run is an idempotent skip). Checksums of the source bundle are verified before anything is read, exactly like a native import.