A Golang CLI that stores Claude Code conversation history as Git Notes and provides visualization.
Claude Code stores conversation transcripts as JSONL (JSON Lines) files at:
~/.claude/projects/<encoded-path>/<session-id>.jsonl
Where <encoded-path> replaces / with - (e.g., /Users/deejay/workspace/foo → -Users-deejay-workspace-foo)
Each line is a JSON object with a type field. Common types:
{
"type": "user",
"uuid": "f782fa84-aa0a-455c-a8d3-70ddb866439e",
"parentUuid": null,
"sessionId": "7c6b617c-ec99-4b6a-8c4c-de0cfadc27e8",
"timestamp": "2026-01-21T16:27:22.780Z",
"cwd": "/Users/deejay/workspace/devcontainer-sync-cli",
"gitBranch": "master",
"version": "2.1.14",
"userType": "external",
"isSidechain": false,
"message": {
"role": "user",
"content": "Your message here..."
}
}{
"type": "assistant",
"uuid": "034f4b03-34a1-4f15-9814-4838dbc034df",
"parentUuid": "f782fa84-aa0a-455c-a8d3-70ddb866439e",
"sessionId": "...",
"timestamp": "...",
"requestId": "req_011CXLqXWXNcShze8pXtVoSd",
"message": {
"model": "claude-opus-4-5-20251101",
"id": "msg_01Sunzzbuit75UU3ddfmgssx",
"type": "message",
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "..."},
{"type": "text", "text": "..."},
{"type": "tool_use", "id": "toolu_...", "name": "Glob", "input": {...}}
],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 5,
"cache_read_input_tokens": 13539
}
}
}{
"type": "user",
"uuid": "0d15dfd6-c33b-4d62-a885-63a15de07171",
"parentUuid": "034f4b03-34a1-4f15-9814-4838dbc034df",
"sourceToolAssistantUUID": "034f4b03-34a1-4f15-9814-4838dbc034df",
"message": {
"role": "user",
"content": [
{"tool_use_id": "toolu_...", "type": "tool_result", "content": "..."}
]
},
"toolUseResult": {
"filenames": ["..."],
"durationMs": 44,
"numFiles": 1,
"truncated": false
}
}{
"type": "system",
"subtype": "turn_duration",
"durationMs": 161634,
"uuid": "...",
"parentUuid": "...",
"timestamp": "..."
}{
"type": "file-history-snapshot",
"messageId": "f782fa84-aa0a-455c-a8d3-70ddb866439e",
"snapshot": {
"messageId": "...",
"trackedFileBackups": {},
"timestamp": "..."
},
"isSnapshotUpdate": false
}{
"type": "summary",
"summary": "Debug Postgres init race condition",
"leafUuid": "8e1a8345-9147-4286-9a60-5a226e4446e8"
}| Field | Description |
|---|---|
uuid |
Unique identifier for this entry |
parentUuid |
UUID of the parent message (forms a tree) |
sessionId |
Session UUID (filename without .jsonl) |
timestamp |
ISO 8601 timestamp |
cwd |
Working directory |
gitBranch |
Current git branch |
version |
Claude Code version |
isSidechain |
Whether this is part of a sidechain |
userType |
"external" for main user |
slug |
Human-readable session name |
- Phase 1: Auto-capture conversations when commits are made via Claude Code hooks
- Phase 2: Resume Claude Code sessions from historical commits
- Phase 3: Web visualization of commit history with embedded conversations
shiftlog/
├── cmd/
│ ├── root.go # Cobra root command
│ ├── init.go # shiftlog init
│ ├── store.go # shiftlog store (hook handler)
│ ├── resume.go # shiftlog resume <commit>
│ ├── serve.go # shiftlog serve
│ └── sync.go # shiftlog sync push/pull
├── internal/
│ ├── claude/
│ │ ├── transcript.go # JSONL parsing
│ │ ├── hooks.go # Hook config management
│ │ └── session.go # Session file management
│ ├── git/
│ │ ├── notes.go # Git notes operations
│ │ ├── graph.go # Commit graph traversal
│ │ └── repo.go # Repository operations
│ ├── storage/
│ │ ├── compress.go # gzip + base64
│ │ └── format.go # StoredConversation struct
│ └── web/
│ ├── server.go # HTTP server
│ ├── handlers.go # API endpoints
│ └── static/ # Embedded HTML/JS/CSS
├── main.go
├── go.mod
└── Makefile
- Detect git repository
- Add PostToolUse hook to
.claude/settings.local.json:{ "hooks": { "PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "shiftlog store", "timeout": 30 }] }] } } - Install Git hooks for automatic note syncing (see below)
- Read PostToolUse JSON from stdin
- Check if
tool_input.commandmatchesgit commit - If not a commit, exit 0 silently
- Get commit SHA via
git rev-parse HEAD - Read JSONL transcript from
transcript_path - Compress with gzip, encode as base64
- Store in Git Note:
git notes --ref=refs/notes/claude-conversations add -f
{
"version": 1,
"session_id": "abc123",
"timestamp": "2025-02-02T10:30:00Z",
"project_path": "/path/to/project",
"git_branch": "feature-branch",
"message_count": 42,
"checksum": "sha256...",
"transcript": "H4sIAAAA... (base64 gzipped JSONL)"
}- Resolve commit reference to SHA
- Read Git Note:
git notes --ref=refs/notes/claude-conversations show <commit> - Decompress and verify checksum
- Write JSONL to Claude's expected location:
~/.claude/projects/<encoded-path>/<session-id>.jsonl - Update
sessions-index.json - Checkout commit:
git checkout <commit> - Launch:
claude --resume <session-id>
API Endpoints:
GET /- Main visualization pageGET /api/commits- List commits with conversation metadataGET /api/commits/:sha- Get full conversation for commitGET /api/graph- Git graph data for visualizationPOST /api/resume/:sha- Checkout and launch Claude session
UI Features:
- Git commit graph (left panel) - branches, commits, highlighting those with conversations
- Conversation viewer (right panel) - human-readable messages, collapsible tool uses
- "Resume Session" button per commit
Tech Stack:
- Go
net/http+embedfor static assets - Vanilla JS + CSS for graph rendering (SVG)
- HTMX for dynamic content loading
.git/hooks/pre-push - Auto-push notes with commits:
#!/bin/bash
# Push claude-conversations notes alongside commits
remote="$1"
git push "$remote" refs/notes/claude-conversations 2>/dev/null || true.git/hooks/post-merge - Auto-fetch notes after pull:
#!/bin/bash
# Fetch notes after merging
git fetch origin refs/notes/claude-conversations:refs/notes/claude-conversations 2>/dev/null || true.git/hooks/post-checkout - Auto-fetch notes on checkout (for clone/switch):
#!/bin/bash
# Fetch notes when switching branches or after clone
git fetch origin refs/notes/claude-conversations:refs/notes/claude-conversations 2>/dev/null || truepush:git push origin refs/notes/claude-conversationspull:git fetch origin refs/notes/claude-conversations:refs/notes/claude-conversations
Useful when hooks aren't installed or for explicit control.
github.com/spf13/cobra # CLI framework
All other functionality uses Go standard library:
compress/gzip,encoding/base64,encoding/jsonembed,net/http,html/templateos/execfor git commands
- Shell out to git (not go-git) - go-git has limited notes support
- gzip + base64 - safe embedding in JSON, good compression for text
- Embedded static assets - single binary distribution
- localhost-only server - security by default
- Git hooks for auto-sync - seamless note syncing with push/pull/checkout
-
Phase 1 Test:
- Run
shiftlog initin a repo - Start Claude Code, make changes, commit
- Verify:
git notes --ref=refs/notes/claude-conversations show HEAD
- Run
-
Phase 2 Test:
- Run
shiftlog resume HEAD~1 - Verify Claude starts with conversation history loaded
- Run
-
Phase 3 Test:
- Run
shiftlog serve - Open http://localhost:8080
- Verify commit graph displays, click commit shows conversation
- Click "Resume" and verify Claude launches
- Run
go.mod- Initialize modulemain.go- Entry pointcmd/*.go- All CLI commandsinternal/**/*.go- Core logicinternal/web/static/*- Web UI assets.claude/settings.local.json- Hook configuration (via init command).git/hooks/pre-push- Auto-push notes.git/hooks/post-merge- Auto-fetch notes on pull.git/hooks/post-checkout- Auto-fetch notes on checkout