Skip to content

share-session sends a person's whole Claude setup - #740

Merged
anirudhkumar-nanonets merged 14 commits into
mainfrom
feat/share-session
Nov 6, 2026
Merged

anirudhkumar-nanonets merged 14 commits into
mainfrom
feat/share-session

Conversation

@anirudhkumar-nanonets

@anirudhkumar-nanonets anirudhkumar-nanonets commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Issue: The team-memory experiment needs each person's whole Claude (their sessions, memory and setup such as MCP connectors) to test whether an agent does better with a teammate's context than alone. trail share-session could only send one session from one folder, and only from the Trail team's own workspace.
Why: One session is too little context, and people won't hand over a month of work unless they can see exactly what goes and take it back.
Fix: trail share-session with no session named now sends a bundle: every Claude Code session from the last 30 days in any folder, plus the setup and the memory notes. It first scrubs everything into a copy on the laptop, shows a summary, and sends only on a yes. Folders can be left out by number, a broken send resumes, and a delete code takes it all back. By default it keeps sending after that yes: each Claude Code session the person starts sends, in the background, the sessions that are new or have grown, into the same bundle. --once sends just the 30 days, and trail share-session <n> still sends one session. The server side is trailhq/trail-cloud#101.

Moves: people who sent their Claude bundle for the experiment, read from trail-cloud's shared_sessions (SELECT count(DISTINCT bundle_id) FROM shared_sessions WHERE kind = 'setup'). 0 on 2026-11-05: /api/cli/shared-sessions is 404 on team.trailhq.com until #101 ships. The new share_bundle event (PostHog db 11) counts how people answer: sent, partial (some folders left out), declined or deleted.

Behind a flag for this release

Everything here is off unless TRAIL_SHARE_SESSION=1 is set, so this can go out on npm and be tested end to end before everyone gets it. The next release removes the check (shareSessionEnabled in src/cloud/share-session.ts, and its one call in src/cli.ts).

  • Without the flag, trail share-session prints one line and exits 1. It reads nothing, scrubs nothing and sends nothing, not even the /allowed check.
  • Always available, flag or not: --stop and --delete <code>, so anyone who tried it can turn it off and take everything back.
  • The background sync isn't gated on the flag. It runs only for someone who said yes to keep sending, which needed the flag, and a hook's environment in Claude Desktop rarely has shell variables. If the hook also checked the flag, a tester's sync would quietly stop there.

To try it: TRAIL_SHARE_SESSION=1 trail share-session

What a person sees

$ trail share-session
· scrubbing sessions: 368 of 368
Your Claude, last 30 days, ready to send to the Trail team

Sessions  368 sessions in 77 folders · Oct 7 – Nov 5 · 148.7 MB zipped
            1  ~/Documents/context-engine                        129 sessions
            2  ~/Documents/assign                                81 sessions
            …  75 more folders, numbered in SUMMARY.md
Setup     72 MCP servers (env values and headers left out) · 61 skills · 18 CLAUDE.md files · 100 hooks · 8 plugins
Memory    78 notes
Secrets   2297 redacted: long secret-looking string ×1242, password or token ×627, .env value ×260, …
You       your name, email and home folder taken out in 479957 places; other people's names stay
Left out  1704 images and files that aren't text
Copy      ~/.trail/share/2026-11-05-1412  every file exactly as it will go, listed in SUMMARY.md
Goes to   the Trail team on team.trailhq.com, for the team-memory experiment, without your name
Delete    any time, with the code you get once it's sent

Send all of this? y · n · or folder numbers to leave out (like 3 7): y
· sent 369 of 369
✓ sent your Claude to the Trail team: 369 items, 148.7 MB · thank you, it goes into the team-memory experiment
· take it all back any time: trail share-session --delete <bundle id>.<secret>

The numbers are from a real dry run on my laptop. Nothing was sent.

Keep sending (the default)

$ trail share-session
Your Claude, last 30 days and from now on, ready to send to the Trail team
…
Keeps on  after this, each Claude Code session you start sends what changed since, scrubbed the same way and kept in a copy you can read; trail share-session --stop stops it
Hook      one entry in ~/.claude/settings.json, so it runs in every folder, in the terminal and in Claude Desktop; stopping takes it out
Delete    any time, with the code you get once it's sent

Send all of this, and keep sending? y · n · or folder numbers to leave out (like 3 7): y
✓ sent your Claude to the Trail team: 369 items, 148.7 MB · thank you, it goes into the team-memory experiment
· and from now on: each Claude Code session you start, in any folder, sends what changed since, kept in ~/.trail/share/sync
· added one hook to ~/.claude/settings.json so this runs in every folder, in the terminal and in Claude Desktop; --stop takes it out
· stop any time: trail share-session --stop
· take it all back any time: trail share-session --delete <bundle id>.<secret>

$ trail share-session
Sending to the Trail team since Nov 5, 14:12: 368 sessions so far.
· last run Nov 5, 16:40: sent 3
· its hook is in ~/.claude/settings.json, so this runs in every folder
  • Upfront. The summary has a Keeps on line saying what happens after the yes, and the question is "Send all of this, and keep sending?". --once drops both and sends just the 30 days. An agent can't turn it on unseen: without a terminal it shows the summary and stops, and --yes sends the copy the person saw. --sync, the old switch, still works as a hidden alias.
  • What runs. On the yes, share-session adds one SessionStart entry to the user-level ~/.claude/settings.json: node "~/.claude/helpers/graft-hooks.cjs" share-sync || exit 0. That's trail's user-level shim (written if missing), in the same fail-open form as trail's other global hooks, so it runs in every folder whether or not that repo ran trail init, in the terminal and in Desktop's Code tab, without relying on PATH. The share-sync event only starts a detached trail _share-sync, so the session never waits. Every session start starts one. There's no time limit; the run's lock keeps a burst of sessions to one run (src/cloud/share-hook.ts, src/cloud/share-sync-state.ts).
  • What goes. Sessions that are new, or have grown since they were last sent, and that nobody has written to for 10 minutes. A session still in use goes on a later run. Each is scrubbed by the same bundleSession, kept under ~/.trail/share/sync/ for the person to read, and sent into the first send's bundle, where Trail replaces the older copy. The setup goes again only when its sha256 changes. Folders left out at the first yes are never sent.
  • Safe to break. sync.json is written after each item, so a run that dies resumes. The lock is taken in one step (open(…, 'wx')), so two runs started together can't both hold it, and a lock over an hour old is taken over. Each run writes down what it did, sent count or error, and the status shows it, so a failing sync doesn't look healthy.
  • The hook stays put. trail share-session while it's on puts the entry back if something took it out. --stop and --delete take it out and leave everything else in the file as it was. A settings file that isn't valid JSON is never touched; the person is told the sync then runs only in repos with trail set up. The entry isn't counted as part of the person's setup, so saying yes doesn't make the setup look changed.
  • Ending it. --stop ends it and says how to take back what went. --delete <code> also ends it.

Defaults I picked

  • What goes.
    • Sessions: every ~/.claude/projects/*/*.jsonl written in the last 30 days (--days changes that), each scrubbed by the same bundleSession as before.
    • Setup: MCP servers from ~/.claude.json (user and per-folder), Claude Desktop's config and each folder's .mcp.json; settings (hooks, permissions, enabled plugins, model); installed plugins; skills, agents and commands; and the CLAUDE.md and AGENTS.md files of the folders the sessions ran in.
    • Memory: every ~/.claude/projects/*/memory/*.md.
  • Nothing that can hold a secret leaves as a value. MCP env and headers and settings env go as names only, and a URL goes as its origin. Every other string goes through redactText and then anonymize, the same as a session. Files over 512 KB are left out.
  • Names. anonymize now hides a distinctive name (6+ characters) wherever it stands: after an escaped \n, after a URL-encoded %2F, or inside a longer word. A short name is still hidden only as its own word, so ian leaves Christian alone. On a real month of sessions, the old rule left the sender's login in 1,123 places and the new one leaves 0. Machine logins like runner and ubuntu aren't treated as names. A line that isn't JSON now has names hidden too; before, it was only redacted.
  • The copy is the consent. Sessions are written as .jsonl.gz, alongside setup.json, manifest.json and a SUMMARY.md that lists every item by numbered folder. What's sent is read back from those files, so the person sees exactly what goes. A no deletes the copy.
  • Agents. Without a terminal it scrubs, prints the summary and the copy's path, and stops. --yes then sends that same copy, never a fresh one the person hasn't seen, and --leave-out "3 7" drops folders. A copy older than a day is prepared and shown again.
  • Upload. One item per request (setup first, then each session), each at most 10 MB zipped, so the 256 MiB pod is fine. Each item that lands is written to sent.json, and the next run sends only the rest, under the same bundle.
  • Delete. Each send gets a bundle id and a random secret. Trail keeps only the secret's sha256. The person gets <bundle id>.<secret> once, and it's also kept in the copy's sent.json. --delete <code> removes everything in that bundle.
  • Who may. Trail decides, through GET /allowed, by the signed-in email's domain (trailhq.com, nanonets.com). The internal-only line says so. A 503 (no file storage on that Trail) reads as "doesn't take shared sessions yet".

Telemetry

New event share_bundle with outcome (sent/partial/declined/deleted/stopped), items_bucket (1 … 200+) and size_bucket (<1MB … 200MB+). All are fixed sets in contract.ts, with a row in TELEMETRY.md and the pinned list in test/telemetry-contract.test.ts. Nothing from the bundle goes in it. Background sync runs send no event. Single-session sends still send session_shared.

Tested

  • 69 tests pass across test/share-session.test.ts, test/share-setup.test.ts (new), test/share-bundle.test.ts (new), test/share-sync.test.ts (new), test/share-hook.test.ts (new) and test/telemetry-contract.test.ts. They cover:
    • setup scrub: env and header values, URL paths and keys never leave; home becomes ~; name and email are hidden
    • bundle: folder numbering, the copy and summary, leave-out, delete codes, resume state
    • the command end to end against a stand-in Trail: no terminal shows and sends nothing, --yes sends the copy as one bundle, a broken send resumes in the same bundle, --delete sends the code, and a bad code is refused
    • anonymize with a name after an escape, %2F and a longer word
    • sync:
      • when a run is due
      • which sessions count as changed (new or grown, and quiet)
      • a run sends into the same bundle, keeps a readable copy, never sends a left-out folder, and sends the setup once
      • a grown session goes again
      • a broken run resumes, and only one runs at a time
      • the hook: one entry, never twice, everything else in settings.json untouched on add and remove, an invalid file left alone
      • end to end: plain trail share-session shows the Keeps on and Hook lines, the hook goes in on --yes and out on --stop, a removed hook is put back by the status, the status shows the last run, --yes turns it on, running it again shows the status (so does the old --sync), _share-sync sends the grown session into the same bundle, and nothing goes after --stop
      • --once sends the 30 days and turns nothing on
    • the flag: off means one line, no request and no copy; --stop works without it
  • Real data, scrubbed locally and never sent: 368 sessions in 77 folders, 148.7 MB zipped. 92 sessions (215 MB) scrub in 25 s.
    • Scanning the copy found 0 occurrences of the sender's login and no real secret.
    • The 114 lines secretReason still flags only trip in their JSON-escaped form: none of their strings is a secret.
  • The full suite: 4160 pass, 9 skipped, and a few fail in graft-to-trail, hosts-trail and upkeep-hooks. Those fail the same way on this branch before this commit, on a laptop with trail 0.28.0 installed globally.

trail check

It exits 1 with 3 to 5 matches, all word overlaps on lines these commits touch in passing:

  • the shared import lines in src/cli.ts, matched to login --code and HOME-and-shim learnings
  • the 503 line in src/cloud/workspace.ts
  • the share_bundle row in TELEMETRY.md
  • the command's action, matched to "refusing every non-git folder"; this command works in any folder and refuses none

None is about sessions, uploads or redaction.

🤖 Generated with Claude Code

@trailhq-graft

trailhq-graft Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

5 areas changed → 3 areas can be affected. 7 dependent symbols, depth 2.
Tests: no test reaches Session Cost Tracking, homePath; 3 areas updated their tests.
Tag: @shhdwi — 7 of 8 areas · @afeddersen — Telemetry Metrics · Rushabh Nagda — MCP Server

flowchart TB
  A0(("Cloud Session Activity<br/>5 symbols"))
  A1(("Always Follow Gate<br/>1 symbol"))
  A2(("MCP Server<br/>1 symbol"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2 reached;
Loading
Can be affected Symbols Nearest hop Reached from
Cloud Session Activity 5 src/cloud/nudge.ts:L112-L117 sessionKey — calls, depth 2 Cloud Sharing, Session Cost Tracking
Always Follow Gate 1 src/notes/always-follow-gate.ts:L76-L90 alwaysFollowBacked — calls, depth 2 Session Cost Tracking
MCP Server 1 src/mcp/server.ts:L1-L255 server.ts — imports, depth 1 Telemetry Metrics
Who knows this code — 3 people across 8 areas
Area Who knows it
Cloud Sharing · changed @shhdwi — 13 commits, last 10d ago
Telemetry Metrics · changed @shhdwi — 1 commit, last 14d ago · @afeddersen — 1 commit, last 2mo ago
homePath · changed @shhdwi — 61 commits, last 10d ago
Trace Redaction · changed @shhdwi — 1 commit, last 15d ago
Cloud Session Activity · affected @shhdwi — 24 commits, last 10d ago
Always Follow Gate · affected @shhdwi — 1 commit, last 11d ago
MCP Server · affected @shhdwi — 5 commits, last 10d ago · Rushabh Nagda — 1 commit, last 16d ago
Session Cost Tracking · changed only you — nobody else has touched these files

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 7 dependent symbols, grouped by area

Cloud Session Activity — 5 symbols in 4 files

  • src/cloud/nudge.ts:L112-L117 — sessionKey (calls, depth 2)
    112: export function sessionKey(repo: string, env: NodeJS.ProcessEnv = process.env, now = Date.now()): string | null {
  • src/cloud/other-repos.ts:L102-L135 — setUpPicked (calls, depth 2)
    127: const synced = await syncRepo(r.checkout, { ...cloud, repos: repoKeys }, { fetch: false, fetchImpl: opts.fetchImpl });
  • src/cloud/pair.ts:L266-L269 — send (calls, depth 2)
    266: private send(body: Record<string, unknown>): void {
  • src/cloud/pair.ts:L308-L367 — waitAndUpload (calls, depth 2)
    309: const now = opts.now ?? Date.now;
  • src/cloud/workspace-sync.ts:L577-L618 — flushActivity (calls, depth 2)
    578: const p = outboxPath();

Always Follow Gate — 1 symbol in 1 file

  • src/notes/always-follow-gate.ts:L76-L90 — alwaysFollowBacked (calls, depth 2)
    79: opts: { env?: NodeJS.ProcessEnv; home?: string; person?: () => boolean } = {},

MCP Server — 1 symbol in 1 file

  • src/mcp/server.ts:L1-L255 — server.ts (imports, depth 1)
    36: * `initialize`'s instructions and the first `tools/list` and `prompts/list`.
Test signal per changed area — 3 ✓ · 2 ✗

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • ✗ Session Cost Tracking — 0 of 1 reached · no test file reaches it
    • not reached: sessionProjectDir
  • ✓ Cloud Sharing — 26 of 83 reached · 4 test files changed here: test/share-bundle.test.ts, test/share-hook.test.ts, test/share-session.test.ts, test/share-sync.test.ts
    • not reached: short, dayLabel, leftOutLine, p, numberFolders, plural, add, readManifest, …49 more
  • ✓ Telemetry Metrics — 2 of 4 reached · 1 test file changed here: test/share-session.test.ts
    • not reached: shareItemsBucket, shareSizeBucket
  • ✗ homePath — 0 of 15 reached · no test file reaches it
    • not reached: askLine, askYes, bundleGz, deleteShared, homePath, hookLine, sendCopy, shareCloud, …7 more
  • ✓ Trace Redaction — 1 of 2 reached · 1 test file changed here: test/share-session.test.ts
    • not reached: now

⚠️ 4 changed files not in the graph (.trail/learnings/2026-10-29-share-session-caps-a-zipped-session-anirudhkumar.md, .trail/learnings/2026-10-30-a-shared-session-drops-the-sender-anirudhkumar.md, .trail/learnings/2026-11-02-tests-that-redact-text-must-not-anirudhkumar.md, TELEMETRY.md) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 23 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

github-actions Bot added a commit that referenced this pull request Oct 29, 2026
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
github-actions Bot added a commit that referenced this pull request Nov 2, 2026
@anirudhkumar-nanonets anirudhkumar-nanonets changed the title Let Trail teammates send a full Claude session share-session sends a person's whole Claude setup Nov 5, 2026
github-actions Bot added a commit that referenced this pull request Nov 5, 2026
github-actions Bot added a commit that referenced this pull request Nov 5, 2026
github-actions Bot added a commit that referenced this pull request Nov 5, 2026
github-actions Bot added a commit that referenced this pull request Nov 6, 2026
github-actions Bot added a commit that referenced this pull request Nov 6, 2026
github-actions Bot added a commit that referenced this pull request Nov 6, 2026
@anirudhkumar-nanonets
anirudhkumar-nanonets merged commit 93a5b0b into main Nov 6, 2026
8 checks passed
github-actions Bot added a commit that referenced this pull request Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant