Phase 1: Multi-profile shared data via symlinks Added: - SharedManager class for symlink orchestration (bin/shared-manager.js) - Auto-migration from ~/.claude/ to ~/.ccs/shared/ on first run - Shared directories: commands/, skills/, agents/ - Windows fallback: copies dirs if symlinks fail Fixed: - Migration logic now detects empty directories - Previously skipped migration when postinstall created empty dirs - Now properly copies from ~/.claude/ when shared dirs are empty Changed: - Instance initialization symlinks to shared dirs instead of copying - Postinstall creates ~/.ccs/shared/ structure automatically - All implementations (Node.js, bash, PowerShell) updated for consistency - Help text includes agents/ in shared data section Technical: - Profile-specific data remains isolated (settings, sessions, todolists, logs) - Migration is idempotent: safe to run multiple times - Cross-platform symlink support with graceful fallback Closes #4
8.6 KiB
Why Session Sharing Fails Across Profiles: Technical Analysis
Overview
Claude CLI sessions cannot be directly shared across CCS profiles due to authentication architecture constraints. When a user starts a thread on a "work" profile and the account expires, resuming from a "personal" profile fails despite the local session file being present.
Core Problem: Thread IDs stored locally, but API-side validation requires matching OAuth credentials. Session "ownership" bound to account that created it, preventing cross-account continuation.
Technical Architecture: How Claude CLI Sessions Work
Session Storage Structure
~/.claude/
├── .credentials.json # OAuth tokens (access, refresh, expiry)
├── projects/
│ └── <project-hash>/
│ └── <sessionId>.jsonl # Thread conversation history
├── session-env/ # Ephemeral runtime state (empty dirs)
└── todos/ # Task tracking
Session File Format (JSONL)
Each thread stored as newline-delimited JSON:
{"type":"user","sessionId":"<uuid>","message":{"role":"user","content":"..."},"uuid":"<msg-id>","timestamp":"..."}
{"type":"assistant","sessionId":"<uuid>","message":{"role":"assistant","content":[...]},"requestId":"req_...","uuid":"<msg-id>","timestamp":"..."}
Key Fields:
sessionId: Thread UUID (e.g.,58921318-0aed-4238-b6cb-d0d6222fc095)message: User/assistant conversation turnsrequestId: API request ID (server-generated, scoped to original auth)- No embedded auth tokens: Credentials stored separately in
.credentials.json
OAuth 2.0 Authentication Model
Credential File (~/.claude/.credentials.json):
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"expiresAt": 1762851849221,
"scopes": ["user:inference", "user:profile"],
"subscriptionType": "max"
}
}
Flow:
- User runs
claude --resume <sessionId> - CLI reads
~/.claude/projects/<project>/<sessionId>.jsonl - CLI reconstructs full conversation history
- CLI submits request to API with OAuth token from
.credentials.json - API validates token + session ownership (inferred)
- API responds (session context maintained client-side only)
API-Side Validation
Claude API is stateless. "Sessions" are client-side constructs:
- Thread ID alone does NOT authorize continuation
- Each request requires valid OAuth token
- Server likely validates session creator identity (undocumented but inferred from org ID headers)
- No server-side session state tied to thread IDs
Why Direct Sharing Fails: Core Technical Blockers
| Component | Transferable? | Blocker |
|---|---|---|
| Session JSONL file | ✅ Yes | None (plain text) |
| Thread ID (UUID) | ✅ Yes | None (identifier only) |
| Conversation history | ✅ Yes | None (embedded in JSONL) |
| OAuth token | ❌ No | Account-specific, expires |
| API authorization | ❌ No | Server-side session ownership validation |
OAuth Token Mismatch
- Work profile session created with
work_oauth_token - Personal profile uses
personal_oauth_token - API rejects requests where token doesn't match session creator (inferred)
Organization ID Boundaries
Anthropic returns anthropic-organization-id header in responses. Sessions may be scoped to:
- User account ID
- Organization ID (for team accounts)
- Subscription tier
Cross-account access would violate organizational isolation.
Session Ownership Enforcement
No documented API for "transferring" session ownership. Thread ID is identifier, not authorization:
- Like a file path: knowing the path doesn't grant read access
- Requires matching credentials to prove ownership
- No "share session" endpoint exists in Claude API
Documentation Gaps
- No cross-account access docs in official Claude API documentation
- No session sharing/transfer APIs in CLI or Agent SDK
- Security model not explicitly documented (inferred from OAuth behavior)
Security Implications: Why This Is By Design
Risks if Session Sharing Were Possible
| Risk | Impact |
|---|---|
| Context Leak | Conversation history exposed to unintended accounts |
| Privilege Escalation | Lower-tier account accessing higher-tier sessions |
| Organizational Data Breach | Work conversations leaked to personal accounts |
| Audit Trail Corruption | Session actions attributed to wrong user |
Current Security Posture
- Credential Isolation: OAuth tokens stored per-profile (CCS architecture)
- POSIX Permissions:
~/.claude/readable only by user (700permissions) - No Session Encryption: JSONL files plain text (relies on OS security)
- Profile Separation: CCS v3.0 login-per-profile prevents credential mixing
Design Intent
Session isolation prevents:
- Cross-account context leakage: Alice's work threads stay with work account
- Subscription boundary violations: Free account can't "resume" Pro sessions
- Organizational data governance: Company data stays within company accounts
Alternative Approaches: What Works Instead
Manual JSONL Copy (Workaround, Not Recommended)
Process:
- Copy session file:
~/.ccs/instances/work/.claude/projects/<project>/<sessionId>.jsonl - Generate new UUID:
new_id=$(uuidgen | tr '[:upper:]' '[:lower:]') - Replace
sessionIdin JSONL:sed 's/<old_id>/<new_id>/g' file.jsonl > new.jsonl - Place in personal profile:
~/.ccs/instances/personal/.claude/projects/<project>/<new_id>.jsonl - Resume:
ccs personal→claude --resume <new_id>
Issues:
- Fragile (breaks if JSONL structure changes)
- No UUID regeneration for
parentUuid, messageuuidfields - Risk of malformed JSON breaking CLI
- Manual process prone to errors
Context Export/Import (PARTIALLY FEASIBLE, Not Implemented)
Concept:
# Work profile (before expiration)
ccs export-context --session <id> --output work-context.json
# Personal profile (after expiration)
ccs import-context work-context.json --new-session
Process:
- Export: Read JSONL → extract message history → strip metadata → save portable JSON
- Import: Parse JSON → generate new session UUID → reconstruct JSONL with personal auth context
- Resume: Use new session ID with personal OAuth token
Advantages:
- ✅ Preserves conversation history
- ✅ New session = clear ownership (personal account)
- ✅ No cross-account authorization issues
- ✅ Explicit user action (no silent credential sharing)
- ✅ Audit-friendly (new session ID)
Status: DEFERRED (not implemented in CCS v3.0)
User Experience Flow (Proposed)
$ ccs export-session 58921318-0aed-4238-b6cb-d0d6222fc095
✓ Exported 47 messages to ~/ccs-export-58921318.json
[Account expires]
$ ccs personal
$ ccs import-session ~/ccs-export-58921318.json
✓ Imported as new thread: a1b2c3d4-5678-9012-3456-789012345678
✓ Resume with: claude --resume a1b2c3d4-5678-9012-3456-789012345678
$ claude --resume a1b2c3d4-5678-9012-3456-789012345678 "Continue from earlier"
[Claude continues with full context under personal auth]
Recommendations: Clear Guidance
REJECT: Direct Session Sharing
Do NOT implement:
- Copying session files between profiles without regeneration
- Reusing thread IDs across accounts
- "Resuming" sessions created by different OAuth tokens
Reason: Violates inferred security model; likely fails API-side validation.
DEFER: Export/Import Feature
Status: Feasible but not critical for CCS v3.0.
Priority: Phase 3 (post-launch).
Effort: Medium (~3-5 days implementation + testing).
Dependencies:
- JSONL parsing/reconstruction logic
- UUID generation for new sessions
- Profile-aware file I/O
Document: Manual Workaround
For Advanced Users (FAQ section):
- Manually copy JSONL file
- Edit
sessionIdfields (usejqorsed) - Regenerate UUIDs for messages (optional but safer)
- Place in target profile's
.claude/projects/directory - Resume with
claude --resume <new_id>
Warning: Unsupported; may break with Claude CLI updates.
Unresolved Questions
-
Does Claude API enforce server-side session ownership? Inferred from OAuth architecture; no official docs confirm. Would require API testing.
-
What is
~/.claude/session-env/used for? Contains empty UUID directories; possibly ephemeral runtime state. Doesn't affect persistence. -
Can malformed JSONL crash Claude CLI? Likely yes (JSON parsing errors). Manual editing risky without validation.
-
What happens if imported session references inaccessible resources? E.g., work git repo personal account can't access. Claude will continue but may request clarification.