bundle

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 29 Imported by: 0

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

View Source
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).

View Source
const (
	ManifestName  = "manifest.json"
	ChecksumsName = "checksums.json"
)

Bundle file names.

View Source
const FormatVersion = "codex-sync-bundle-v1"

FormatVersion identifies the bundle layout. Bump this on breaking changes.

Variables

This section is empty.

Functions

func DirExists

func DirExists(p string) bool

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

func ParseSince(s string) (time.Time, error)

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

func StripImagesJSONL(plain []byte) (out []byte, count int)

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

type CWDMapping struct {
	Old string
	New string
}

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

type Checksums map[string]string

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

type InspectResult struct {
	Manifest  Manifest
	Checksums Checksums
	Entries   []string
}

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

type TranslateOptions struct {
	BundlePath string
	TargetTool agent.Kind
	DryRun     bool
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL