cc-session-io
v0.1.1
Published
Read, write, and create Claude Code session files (unofficial)
Maintainers
Readme
cc-session-io
Read, write, and create Claude Code session files.
Can create new session history files or read/write existing ones that will work with /resume in Claude Code. Useful for importing conversation history from another coding agent to continue it with claude.
No runtime dependencies. Not affiliated or supported by Anthropic, the makers of Claude Code.
Install
npm install cc-session-ioRequires Node >= 20.
Simple CLI Demo
# Create new session
cc-session create -p /my/project
# Add messages to session
cc-session add user "Hello!" -p /my/project -s <id>
cc-session add assistant "Hi, how can I help?" -p /my/project -s <id>
# Read conversation
cc-session read -p /my/project -s <id>
# List sessions
cc-session list -p /my/projectOptions:
-p <path>- Project path (required)-s <id>- Session ID (required for add/read)
Quick Start
Create a new session
import { createSession } from 'cc-session-io';
const session = createSession({ projectPath: '/path/to/project' });
session.addUserMessage('Refactor the auth module');
session.addAssistantMessage([{ type: 'text', text: 'I will refactor the auth module.' }]);
session.save();
// Resume it: claude --resume <session.sessionId>Append to an existing session
import { openSession } from 'cc-session-io';
const session = openSession({
sessionId: 'existing-uuid-here',
projectPath: '/path/to/project',
});
// Existing messages are available
console.log(`${session.messages.length} messages loaded`);
// Append new messages — they chain onto the last existing record
session.addUserMessage('One more thing...');
session.addAssistantMessage([{ type: 'text', text: 'Sure, what is it?' }]);
session.save(); // appends to the existing JSONL fileAPI
createSession(opts): Session
Creates a new session. Generates a UUID session ID and resolves the JSONL path.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| projectPath | string | required | Absolute path to the project. Determines where the JSONL file is written. |
| claudeDir | string? | ~/.claude | Override the Claude config directory. |
| cwd | string? | projectPath | Working directory written into records. |
| gitBranch | string? | "HEAD" | Git branch name written into records. |
| version | string? | "2.1.83" | Claude Code version to claim. |
| model | string? | "claude-sonnet-4-6" | Default model for assistant messages. |
openSession(opts): Session
Opens an existing session by ID for reading or appending.
| Option | Type | Description |
|--------|------|-------------|
| sessionId | string | UUID of the session. |
| projectPath | string | Absolute path to the project. |
| claudeDir | string? | Override the Claude config directory. |
readSession(jsonlPath, projectPath?): Session
Reads a session directly from a JSONL file path.
Session
Properties
| Property | Type | Description |
|----------|------|-------------|
| sessionId | string | UUID of the session. |
| projectPath | string | Project path. |
| jsonlPath | string | Absolute path to the JSONL file. |
| records | JsonlRecord[] | All JSONL records (including non-message types). |
| messages | (UserRecord \| AssistantRecord)[] | Only user and assistant records. |
session.addUserMessage(text): string
Adds a user text message. Returns the record's UUID.
session.addAssistantMessage(content, opts?): string
Adds an assistant message with the given content blocks. Returns the record's UUID. Content blocks are written as-is — any tool_use blocks you provide keep their original id values. (This matters when replaying real API responses; addToolCalls() generates IDs for you, but addAssistantMessage() does not.)
session.addAssistantMessage([{ type: 'text', text: 'Hello!' }]);
session.addAssistantMessage(
[{ type: 'text', text: 'Done.' }],
{ model: 'claude-opus-4-6' },
);
// Pass through real tool_use blocks with their original IDs
session.addAssistantMessage([
{ type: 'tool_use', id: 'toolu_01ABC...', name: 'Read', input: { file_path: '/foo' } },
]);| Option | Type | Default |
|--------|------|---------|
| model | string? | Session default model |
| stopReason | string? | Auto-detected: "tool_use" if content has tool_use blocks, "end_turn" otherwise |
session.addToolResults(results): string
Adds a user message containing tool results. Each toolUseId must match the id of a tool_use block from the preceding assistant message — this is how Claude Code pairs requests with responses.
// After an assistant message with tool_use blocks:
session.addToolResults([
{ toolUseId: 'toolu_abc', content: 'file contents here' },
{ toolUseId: 'toolu_def', content: 'command output', isError: true },
]);session.addToolCalls(calls, opts?): void
Convenience method that creates the full tool call round-trip: assistant tool_use message, user tool_result message, and optionally a final assistant response.
// Single tool call with response
session.addToolCalls(
[{ name: 'Read', input: { file_path: '/foo.ts' }, result: 'file contents' }],
{ response: [{ type: 'text', text: 'I read the file.' }] },
);
// Multiple parallel tool calls
session.addToolCalls([
{ name: 'Read', input: { file_path: '/foo.ts' }, result: 'contents of foo' },
{ name: 'Bash', input: { command: 'ls' }, result: 'README.md\nsrc/' },
], { response: [{ type: 'text', text: 'I read the file and listed the directory.' }] });
// Tool call without a follow-up response (leaves the turn open)
session.addToolCalls([
{ name: 'Bash', input: { command: 'npm test' }, result: 'all tests passed' },
]);| ToolCallSpec field | Type | Description |
|---------------------|------|-------------|
| name | string | Tool name (e.g. "Read", "Bash", "Edit") |
| input | unknown | Tool input parameters |
| result | string \| ContentBlock[] | Tool output |
| isError | boolean? | Whether the tool call errored |
session.importMessages(messages): void
Bulk import an array of Anthropic API-shaped messages. Dispatches each to the right internal method based on role and content type. Useful when replaying API responses or syncing from another provider.
session.importMessages([
{ role: 'user', content: 'Read the config file' },
{ role: 'assistant', content: [
{ type: 'tool_use', id: 'toolu_abc', name: 'Read', input: { file_path: '/config.json' } },
]},
{ role: 'user', content: [
{ type: 'tool_result', tool_use_id: 'toolu_abc', content: '{"port": 3000}' },
]},
{ role: 'assistant', content: [{ type: 'text', text: 'The config sets port to 3000.' }] },
]);Dispatch rules:
assistant→addAssistantMessage()(string content wrapped in a text block)userstring →addUserMessage()userarray withtool_resultblocks →addToolResults()userarray with onlytextblocks →addUserMessage()(text joined with newlines)
session.save(): void
Writes all pending records to disk. Creates the JSONL file and parent directories if needed. Appends if the file already exists.
Low-level JSONL utilities
import { parseJsonl, parseJsonlFile, serializeRecord, serializeJsonl } from 'cc-session-io';
const records = parseJsonlFile('/path/to/session.jsonl');
const jsonlString = serializeJsonl(records);Path utilities
import { projectPathToHash, getProjectDir, getSessionPath } from 'cc-session-io';
projectPathToHash('/Users/me/project');
// => '-Users-me-project'
getSessionPath('uuid-here', '/Users/me/project');
// => '/Users/me/.claude/projects/-Users-me-project/uuid-here.jsonl'Content Block Format
All content blocks use the Anthropic API format. If you're coming from another agent framework, note the naming:
| This library (Anthropic API) | Other conventions |
|------------------------------|-------------------|
| tool_use | toolCall, function_call |
| tool_result | toolResult, function_response |
| tool_use_id | toolCallId |
| stop_reason: "tool_use" | finish_reason: "function_call" |
How It Works
Claude Code stores sessions as JSONL files in ~/.claude/projects/-<path-hash>/. Each line is a JSON record. User and assistant messages form a linked list via uuid/parentUuid fields.
This library writes JSONL files that match the real Claude Code format. On resume, Claude Code replays the stored messages to rebuild context. No SQLite database or session index is required — the JSONL file alone is sufficient.
Minimum viable record
The library writes full-fidelity records with all fields, but Claude Code only requires these for resume:
{"type":"user","uuid":"<uuid>","parentUuid":null,"sessionId":"<uuid>","timestamp":"<ISO 8601>","message":{"role":"user","content":"hello"}}
{"type":"assistant","uuid":"<uuid>","parentUuid":"<user-uuid>","sessionId":"<uuid>","timestamp":"<ISO 8601>","message":{"id":"msg_...","type":"message","role":"assistant","model":"claude-sonnet-4-6","content":[{"type":"text","text":"hi"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}Everything else (cwd, version, isSidechain, slug, entrypoint, gitBranch, userType) is optional. See docs/claude-code-sessions.md for full format documentation and research findings.
Testing
npm test # unit + integration tests
npm run smoke # end-to-end: creates a session, resumes with claude CLI