session-pack
Skill1 starsChanged 2 months agoMITView on GitHub
This skill should be used when the user wants to hand a Claude Code session to another person or machine - "session pack", "export/import a session", "transfer conversation context", "hand this conversation to someone else", "ccpack", "pack this session", "unpack and resume", "pack in token-saving mode", "lightweight pack", "lean pack" - or when the user wants to track a conversation in git alongside its code so a team can share, fork, and cross-reference it over time - "track this conversation in git", "share this conversation in the team repo", "collect conversations from several repos into one hub", "branch/fork the conversation", "conversation hub", "sync this conversation to git", "track this conversation in a repo", "fork this conversation". It bundles a session transcript plus its memory, agents, skills and touched files into a portable .ccpack, tokenizing every environment-specific value (paths, identity, API keys) so the receiving environment re-binds them at unpack time and resumes with `claude --resume` - optionally in a token-saving `--lean` mode that abridges the bulk (file bodies, long tool results, settled reasoning) so resuming costs the receiver a fraction of the context - or, in continuous mode, commits the transcript onto dedicated git refs (`init`/`sync`/`adopt`/`render`/`why`) that can be forked, promoted, archived, and aggregated across repos into a shared hub.
- Reads credentials
1 other project uses a version of this.
Adopted by
Most recently taken up by
- ten-builder/ten-buildertheir own version, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- ten-builder/ten-builder★ 111same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 1 is distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 19
Two user-facing entry points wrap this skill: Prefer pointing the user at them: /pack (skill pack) and /unpack (skill unpack).
- Very specific: line 26
Identify the session: If the user didn't name one, list candidates by recency: ~/.claude/projects/<cwd with every non-alphanumeric replaced …
- Very specific: line 133
Add --strip-unknown only when inspect flagged record types this CLI does not know.
- Very specific: line 134
Add --force only to overwrite existing project files, and say so before doing it.
- Very specific: line 336
The hub mirror is a second push, not a second commit: Run sync --all --publish to origin, then sync --publish --remote hub.
- Very specific: line 344
Divergence is normal, not an error: When two people both continue from the same point, sync --fork <name> branches from the commit the …
- Distinctive: line 379
Repos grow until
git gc: Every sync stores a full snapshot blob. - Very specific: line 400
Write JSONL as UTF-8 without BOM and LF endings.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Session Pack
Move a Claude Code session between users or machines at full fidelity: the real
transcript travels, so claude --resume restores the conversation exactly as it was.
Everything environment-specific is tokenized into named binding slots at pack time and re-bound from the receiving environment at unpack time. Secrets are never transported — only the slot and a redacted hint travel; the receiver's own env var supplies the value.
The bundled script is scripts/ccpack.py (Python 3.9+, no dependencies). Always invoke it
by absolute path from this skill's directory.
Two user-facing entry points wrap this skill. Prefer pointing the user at them:
/pack (skill pack) and /unpack (skill unpack). They are disable-model-invocation,
so only the user can trigger them — this skill is the auto-discoverable one and holds the
shared script and reference material.
Packing (sender)
- Identify the session. If the user didn't name one, list candidates by recency:
~/.claude/projects/<cwd with every non-alphanumeric replaced by '-'>/<sessionId>.jsonl. The current session id is in the transcript'ssessionIdfield. - Build the bundle:
python <skill-dir>/scripts/ccpack.py pack <sessionId> -o <name>.ccpack --with-files--with-filessnapshots files under the project root that the transcript actually touched, found viafile_path/notebook_path/pathtool inputs. Include it unless the receiver already has the repo — without it, the conversation will reference files that do not exist on the other side.--deep-filesadditionally harvests paths appearing in tool results (Grep, Glob, Bash output) — files the model reasoned about but never opened directly. Broader and noisier; use when the conversation did a lot of searching.--max-files Ncaps collection (default 200). Anything dropped is counted and reported, never silently truncated.--with-historyalso carries~/.claude/file-history/<id>(undo versions). Rarely needed.--no-genericdisables thekey = valueheuristic if it is producing false positives.--leanturns on token-saving mode (below). Ask the user which they want before packing — it is their call, and the two produce genuinely different bundles.
- Review
REPORT.mdin the bundle and show its slot table to the user. Tool results are inlined verbatim in transcripts, so pattern matching is best-effort. Tell the user to confirm nothing proprietary remains before sending. This step is not optional. - Relay the file counts.
packreportspacked / missing / skipped / truncated. Missing means the transcript references a file that no longer exists even on the sender's machine — the receiver will hit the same gap, so say so rather than letting it surface later as a confusing failure.
Token-saving mode (--lean)
Resuming a bundle replays the whole transcript into the receiver's context, so a packed
session is billed again on every turn after the resume. Most of a transcript is not
conversation: file bodies inside Write/Edit inputs, tool results the model already
summarized in prose, reasoning from turns that are long settled. --lean replaces those
with a marked [ccpack lean: N chars omitted] gap and leaves what was said alone.
Measured on two real sessions (2,239 and 514 records): -1723% / -5971% / -71~77% of
the replayed context at the three levels, with the conversation itself untouched at all
three. A resume of the smaller one cost $0.92 full-fidelity and $0.31 at balanced.
The figure depends on what the session did - pack prints the real one every time.
| flag | effect |
--lean | on, at level balanced |
--lean-level light | keeps reasoning, cuts only the outliers |
--lean-level balanced | default: drops settled reasoning, dedupes re-read files |
--lean-level max | leaves little beyond what was said and what was run |
--lean-keep-tail N | records at the end left byte-for-byte alone (default 20) |
--lean-max-result N | per-tool-result character cap, overriding the level |
--lean-keep-thinking | keep every thinking block in full |
Report the before/after numbers the command prints — context ~X -> ~Y tokens (-Z%) — and
the per-category table. They are the point of the mode, and REPORT.md and the receiver's
inspect both repeat them, so the bundle is never quietly abridged.
Why an abridged transcript still resumes:
- Records are trimmed in place, never removed, so the
parentUuidtree and everytool_use/tool_resultpair survive untouched.file-history-*records are the one exception — they carry no uuid and take no part in the message tree. - The tail is protected. The last
--lean-keep-tailrecords are byte-for-byte identical, and thinking blocks are kept for the whole final turn: that is the turn the API replays verbatim when a resume lands mid tool-loop, and a missing block there is a hard error, not a lost detail. packruns the abridged transcript throughverifybefore writing it and falls back to full fidelity if it would not pass. A bundle that fails at resume time, on someone else's machine, is worse than a large one.
Pair it with --with-files: the file bodies cut out of Write/Edit inputs are exactly
what files/ carries, so the content is still in the bundle — and the gap marker says so.
Do not use it when the record itself is the deliverable: an audit, an incident review,
or a conversation someone will later sync into a repository. Continuous mode is
append-only, and an abridged transcript is not a valid continuation of the full one —
sync says so out loud if it finds lean markers in the transcript it is about to commit.
Unpacking (receiver)
Dry-run first — never unpack blind:
python <skill-dir>/scripts/ccpack.py inspect <bundle>.ccpack --cwd <target project root>Report to the user: CLI version delta, unknown record types, which slots resolve automatically, which need
--bind, plus the two compatibility sections below.Capabilities.
inspectdiffs every tool, MCP server, subagent type and skill the transcript called against what this environment provides.MISSING— an MCP server or agent the transcript used that does not exist here. Resuming will produce dead tool calls. Offer to install the agent definition or configure the MCP server before resuming.CHECK— not found on disk but possibly built-in or plugin-provided. Usually benign.
Files.
inspectcompares carried file hashes against the target and reportsto restore/differ locally/already missing at pack time. ADIFFERSfile is never overwritten without--force; ask the user which copy should win.- Fill unresolved slots. Slots with strategy
env:NAMEread the receiver's environment variable;promptslots need an explicit value. Ask the user for each one rather than guessing, then pass--bind SLOT=VALUE(repeatable). Leaving a slot unbound is allowed — it stays a literal placeholder in the transcript. - Install:
Addpython <skill-dir>/scripts/ccpack.py unpack <bundle>.ccpack --cwd <target> [--bind ...] [--strip-unknown]--strip-unknownonly wheninspectflagged record types this CLI does not know. Add--forceonly to overwrite existing project files, and say so before doing it. - Hand off the resume command printed by the script. Do not try to resume for the user —
--resumeis interactive and must run in their own terminal:cd <target> claude --resume <new session id>
What unpack adjusts
| Adjusted | How |
| Project paths, home dir | slot re-rendered in the original slash/escape style at the target root |
| Identity (email, userID, machineID, username) | replaced with the receiver's own values. Only the account's address re-binds; a third-party address quoted in the conversation stays a placeholder (rewriting it would silently change what was said) and can be filled with --bind |
| API keys, tokens, JWTs, private keys | re-read from the receiver's env vars, or --bind |
CLI version field | rewritten to the locally installed claude --version |
gitBranch | re-detected in the target repo (empty if not a repo) |
sessionId | fresh UUID, applied to the filename and every record |
| Model | reported, never auto-rebound: --model picks what to resume with, --rewrite-model restamps past turns |
| Unknown record types | reported; dropped only with --strip-unknown |
| Missing tools / agents / MCP servers | reported before resume, so dead tool calls are fixed up front |
| Referenced files | restored where absent; locally-modified copies kept unless --force |
| Files from outside the sender's project | restored into .ccpack-external/ (added to .gitignore), with the transcript's paths rewritten to match; decline them all with --no-external |
Memory (memory/), CLAUDE.md, and project .claude/agents|skills|commands are placed
into the target; referenced file snapshots are restored only where the file is missing.
Memory travels through the same tokenizer as the transcript — it is prose about the
sender's environment — and unpack re-binds it to the receiver's values.
Continuous mode: a conversation tracked in git
pack/unpack hand a session over once. init/sync/adopt keep one in a
repository, so several people work the same conversation the way they work the same
code. Same tokenizer, different plumbing: git clean/smudge filters bind on checkout and
canonicalize on commit, so every participant's copy shows their own paths while the
committed blob shows slots.
Two remotes are required before offering this mode — check git remote -v before
running init:
- A code remote (
origin, or any dedicated remote) — continuous mode exists so other people can read, adopt, or fork a conversation, which only works if the code it points to is fetchable by someone besides the current machine. No remote at all (local-only repo, or not a repo yet): don't start continuous mode. Tell the user it needs a remote repository first, and offer to help them create one and rungit remote add origin <url>— a project of its own, never the shared hub below (a code branch pushed there collides with every other orphan project doing the same thing, since hub refs are collision-proofed only for conversations, not forrefs/heads/*). If they don't want to set one up, usepack/unpackinstead — it carries the files themselves and needs no remote. - A
hubremote (optional, recommended for teams) — every conversation is published here too, regardless of which repo it came from. Ifgit remote -vshows nohubentry, ask the user for their team's hub URL and add it:
Teams that standardize on one hub should pin its URL right here in this file, so the remote can be added without asking. If the user has no hub, skip the hub publish step — origin alone is a complete setup.git remote add hub <hub-repo-url>
ccpack init --name token-grace # link this session to a conversation ref
ccpack sync --all --publish -m "..." # origin: code + conversation, pushed atomically
ccpack sync --publish --remote hub # hub: same conversation commit, no code
# on the other machine
git clone <url> && cd <repo>
ccpack init # no local session needed - read mode
ccpack branches # what conversations exist
ccpack adopt alice/token-grace # -> ~/.claude/projects/...; prints the resume command
ccpack render alice/token-grace # or just read it
ccpack why src/auth.py:42 # which conversation produced this lineThe conversation never enters your working tree. It lives only under
refs/ccsession/<you>/<name>, so main carries code and nothing else. That is the
default (--sidecar), and it is what makes this cheap:
| conversation on a ref | conversation on main | |
git clone for someone who doesn't use it | +0 bytes | +606KB per conversation |
git status with 25 conversations in the repo | 0.03s | 3.19s |
| conversations per repo | unlimited | 1 — the path is fixed |
| git filters needed | none | .gitattributes + config in every clone |
--on-main selects the old layout: the transcript becomes a tracked file, visible in PR
diffs at the cost of every clone carrying it and every git status paying the filter.
Reasonable for a small team where the conversation is the documentation; it does not
scale.
Reading a conversation
render is how a conversation reaches a person. By volume a transcript is reasoning
blocks and tool results; by meaning it is what the two of you said, so rendering drops
roughly 95% of the bytes on purpose and rewrites canonical slots into readable paths —
nobody reviewing a pull request has a reason to know what __CCPACK_PROJECT_ROOT_ESC__
is.
ccpack render # this repo's conversation
ccpack render alice/token-grace --turns 3 # someone else's, last 3 exchanges
ccpack render <ref> --since HEAD~1 --format md # just this commit's turns, for a PR comment--since is the one that matters for review: it emits only the turns a given commit
added, so a 3.5MB session still produces a comment a person will read. Rendered blocks
carry their record index (· r12), so a specific turn can be quoted and found again.
The reverse link: ccpack why
sync --all attaches a note (refs/notes/ccsession) to each code commit it creates,
naming the conversation ref and the record range that produced it. Notes attach after
the commit exists, so the commit hash never changes, and they live on their own ref -
nothing in the worktree, opt-in like everything else.
ccpack why src/auth.py:42 # blame -> commit -> note -> those turns, rendered
ccpack why src/auth.py # last commit touching the fileinit installs the notes fetch refspec (to refs/notes/<remote>/ccsession, so a fetch
never overwrites notes you have not pushed), and --publish pushes the notes ref -
outside the atomic pair, merging first if someone else published notes in between.
Commits made without ccpack sync --all carry no note; why says so rather than
guessing.
When two people continue the same conversation
ccpack sync -m "..." # -> "diverged 6 record(s) in", refuses
ccpack sync --fork sidequest -m "..." --publish
ccpack branches # yours and everyone else's
ccpack render main sidequest # the fork, side by side: shared prefix once,
# then what each line did after it
ccpack gc --dry-run # refs already contained elsewhereConversations are never merged - a 3-way merge fabricates an exchange nobody had - so
render <refA> <refB> is how diverged lines rejoin: in the reader, not in storage.
The ref lifecycle
ccpack log # how this conversation grew, sync by sync
ccpack tag v1.0 # immutable: this conversation produced v1.0
ccpack promote token-grace # participant line -> refs/ccsession/shared/<name>
ccpack archive old-experiment # dead line -> refs/ccsession/archive/<who>/<name>tags/, shared/ and archive/ are reserved namespaces with their own rules: a tag
is never rewritten, promote is fast-forward only (rewinding the team line is a human
decision), and branches hides archive/ unless asked (--archived). None of them
are ever gc candidates - a tag points at an old commit on purpose, which is exactly
what gc's "contained elsewhere" test would otherwise condemn.
For a PR bot, why --commit <sha> skips the file/blame step - a bot iterates commits.
examples/pr-conversation-comment.yml is a ready GitHub Actions workflow that comments
each PR with the conversation that produced its commits.
Published branches live under refs/ccsession/<you>/<name>, following Gerrit's
refs/changes/* and GitHub's refs/pull/*. They are not branches: hundreds of them
cost nothing in anyone's git branch -a, and init adds the fetch refspec that makes
them visible to people who want them. gc drops a ref only when another ref is strictly
ahead of it — never a duplicate, and never a commit.
The shared hub
refs/ccsession/* lives inside the repo it was created in by default. Publishing to the
hub remote (set up per the prerequisite above) is what makes a
conversation readable from one place regardless of which repo it came from — no code
ever goes there:
ccpack sync --all --publish -m "..." # origin: code + conversation, as usual
ccpack sync --publish --remote hub # hub: conversation onlyAny --remote other than origin is treated as a hub, not a second origin: the code
branch and refs/notes/ccsession are dropped from that push, and the destination ref is
prefixed with the source repo's path — refs/ccsession/<repo-slug>/<you>/<name> — so
alice/incident-1234 from two different repos never collides once both land in the hub.
# reading from the hub — no init needed in the source repos for this
git clone <hub-repo-url> && cd <hub-clone>
ccpack init && git fetch origin 'refs/ccsession/*:refs/ccsession/*'
ccpack branches # <repo-slug>/<you>/<name>
ccpack render <ref>- A ref's row in
branchesis only its tip. Sidecar syncs append to the same ref instead of branching, so one row can hide several past sync checkpoints.branchesprints asyncscount per ref and, when any ref has more than one, a trailer telling you whichccpack log <ref>to run — always read that trailer before telling someone "there's 1 conversation up there," since the ref itself may still be several checkpoints deep. - The hub clone has no code.
adoptneeds the original source repo — fetch fromhubthere and adopt from that checkout, not from the hub clone. whyonly works from the origin clone. The reverse-link notes are excluded from the hub push on purpose, so tracing a code line back to its conversation has to happen where the code actually lives.- The hub mirror is a second push, not a second commit. Run
sync --all --publishto origin, thensync --publish --remote hub; the second one has nothing new to commit by construction and pushes the current tip anyway. It reports "nothing new since …" followed by thepublished:line — both are expected. - Append-only is enforced.
syncclassifies the working copy againstHEADas ahead (ordinary append), behind (pull first), or diverged, and refuses to overwrite committed turns. - Divergence is normal, not an error. When two people both continue from the same
point,
sync --fork <name>branches from the commit the conversation actually left the shared line at — not fromHEAD, which would carry the other person's turns underneath yours. - Mid-turn commits are blocked. Claude Code appends while a turn runs; committing
then stores a tool call whose result does not exist yet.
syncwaits for the file to settle (--wait, default 10s) and refuses an unfinished turn.--nowoverrides. - Run
syncin the background, never in the foreground. A foreground shell call is itself the trailing tool call the settle check is waiting on — its own result cannot exist until it finishes, so it can never resolve inside--waitno matter how long that is or how many turns pass; every foreground attempt fails the same way, every time. Launching that same call in the background records "started" as its own result right away, which is what settles the file — the real sync then runs against an already-quiet transcript and succeeds normally. Read the background output for the actual[ok]/[!]line before reporting success; a launched job is not a finished one. Don't default to asking the user to type a!-prefixed command instead — that's a fallback for a tool environment that genuinely cannot background a shell call, not the normal path. - The last turns can be left out.
sync --until Ncommits records 1-N only - for when the tail is a failed experiment. A prefix keeps append-only intact, so the next sync just appends the rest. Record numbers are therNindicesrendershows. Sidecar only. - Secrets go one way. They are tokenized on the way in and never rendered on the way
out: no checkout has anyone else's key. They stay visible placeholders, and
syncreports how many. - Third-party addresses stay literal here, unlike in a bundle. Tokenizing them is irreversible, so inside a shared repo it destroys conversation content rather than protecting anyone. The account address is still tokenized — it differs per person.
- Usernames stay literal here too. They are ordinary words: when a participant is named "bob" and the conversation mentions a bob, tokenizing made the canonical form participant-dependent (false divergence) and renamed people in checkouts. Paths that carry the username are already covered by the home/project slots.
- Fetch is fast-forward only for conversation refs. A forced refspec rewound a locally-advanced ref to the remote's older commit, silently. A genuinely diverged ref (same person, two machines) is reported by fetch instead of clobbered.
- Repos grow until
git gc. Every sync stores a full snapshot blob; delta compression happens at gc time (measured: 34 syncs = 5x the transcript, 95% reclaimed by onegit gc). Run it occasionally, or let git's auto-gc get there. - Each participant keeps their own local session id;
__CCPACK_SESSION_ID__is what the repo stores..ccsession/config.jsonholds the local binding and is gitignored. ccpack verifyis the pre-commit gate: conflict markers, brokenparentUuid, orphanedtool_result, live secrets. A merged transcript that fails it is not resumable, and nothing downstream would notice until someone tried.
Deliberately not transported
State these to the user when relevant — they are security boundaries, not oversights:
allowedTools/hasTrustDialogAccepted— the receiver must grant permission themselves.- MCP OAuth tokens — only server definitions are listed in the manifest; re-authenticate locally.
~/.claude/.credentials.json,sessions/— account credentials and live process registry.
Notes
- Transcript format is Claude Code's internal storage, not a supported interchange format.
inspectwarns on a minor-version mismatch; treat a major-version gap as unsupported. - Write JSONL as UTF-8 without BOM and LF endings. The script does; hand-editing in
PowerShell with
>orSet-Contentwill corrupt non-ASCII text. parentUuidforms the message tree — never hand-trim records out ofsession.jsonl, or the chain breaks. Use--strip-unknowninstead.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

