https://github.com/gradigit/claude-pager
Fast C transcript pager for Claude Code: no blank Ctrl-G screen, clickable links/files, queued prompt composer, TurboDraft fast path.
https://github.com/gradigit/claude-pager
c claude-code developer-tools macos pager performance terminal turbodraft
Last synced: 3 months ago
JSON representation
Fast C transcript pager for Claude Code: no blank Ctrl-G screen, clickable links/files, queued prompt composer, TurboDraft fast path.
- Host: GitHub
- URL: https://github.com/gradigit/claude-pager
- Owner: gradigit
- License: mit
- Created: 2026-02-19T12:31:18.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-12T19:59:35.000Z (3 months ago)
- Last Synced: 2026-03-13T00:42:16.908Z (3 months ago)
- Topics: c, claude-code, developer-tools, macos, pager, performance, terminal, turbodraft
- Language: C
- Size: 548 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# claude-pager
A scrollable terminal pager for Claude Code session transcripts. Press **Ctrl-G** in Claude Code and your conversation history renders in the terminal while your GUI editor is open.
claude-pager solves two major Ctrl-G pain points:
- Claude Code’s TUI going blank while an external GUI editor is open
- Broken Cmd-click behavior on long wrapped links in terminal output
It does this with a native C pager + OSC-8 hyperlinks, so wrapped URLs and file paths stay clickable.
The runtime is a single compiled C binary — no Python, no Node, no runtime dependencies.
## Before vs After: clickable links and file paths
Before
After (claude-pager)
claude-pager shortens and wraps links and file paths into clickable OSC-8 hyperlinks, and keeps mouse scrolling just like regular Claude Code session context.
## What's New in v2

- **Built-in queued prompt composer** right inside the pager, so Ctrl-G no longer means read-only transcript context
- **Multiline prompt drafting** with **Shift+Enter**, plus queue cycle/edit/remove controls
- **Clipboard + drag/drop attachments** that turn pasted files and images into `@/absolute/path` references
- **TurboDraft fast path** for low-latency session open/close over a direct Unix socket
- **Interactive terminal ergonomics**: scroll wheel browsing, click/Cmd-click links, and Shift-drag text selection
## Install (quick start)
### One-liner
```sh
curl -sSL https://raw.githubusercontent.com/gradigit/claude-pager/main/install.sh | bash
```
This clones the repo to `~/.claude-pager`, builds the binary, sets the `editor` in `~/.claude/settings.json`, preserves your original editor as `env.CLAUDE_PAGER_EDITOR`, writes `env.CLAUDE_PAGER_EDITOR_TYPE` (`tui`/`gui`), and installs the required Claude hooks for transcript lookup + queued prompt draining. No shell config changes needed.
### AI agent install
Paste the repo URL into Claude Code or any AI coding agent. The [agent instructions](#agent-instructions) below have everything it needs to install and configure claude-pager automatically.
Important: Claude hook entries must use hook-group objects with a nested `hooks` array. Flat hook objects like `{"type":"command","command":"..."}` directly under `hooks.SessionStart` or `hooks.Stop` are invalid in current Claude releases.
### Prebuilt binaries
If you don't want to compile locally, download the latest release assets from the GitHub releases page:
- `claude-pager--macos-arm64.tar.gz` (Apple Silicon)
- `claude-pager--macos-x86_64.tar.gz` (Intel)
- `checksums.txt`
Then verify:
```sh
shasum -a 256 -c checksums.txt
```
Extract the archive and use `bin/claude-pager-open` as your Claude Code editor path.
## ⚡ Performance
claude-pager is tuned for low-latency Ctrl-G flow, with instrumented timings from a production benchmark run (52 cycles total, 2 warmup excluded, 50 measured).
### claude-pager internal rendering timings
| Component | Median |
| --- | ---: |
| Claude Code exec overhead | **6.3ms** |
| claude-pager first draw | **2.7ms** |
| Terminal-ready probe | **0.04ms** |
### Ctrl-G flow timings (TurboDraft fast path)
| Metric | Median | p95 |
| --- | ---: | ---: |
| Ctrl-G → editor window visible | **60.1ms** | **76.1ms** |
| Cmd-Q → back to Claude Code | **53.1ms** | **61.3ms** |
These Ctrl-G flow timings are measured with TurboDraft using claude-pager’s direct Unix-socket fast path. Other popular GUI editors go through the generic launch/wait path and typically do **not** hit sub-100ms Ctrl-G end-to-end flow timings.
claude-pager itself is extremely fast; most remaining end-to-end latency is outside claude-pager (external editor + window rendering path).
## ✨ Speed-of-thought editing with TurboDraft
If you want the lowest-latency prompt editing feel, use [**TurboDraft**](https://github.com/gradigit/turbodraft) (the sister tool) with claude-pager.
- claude-pager: fast transcript context + Ctrl-G flow
- TurboDraft: near-instant editing experience once the editor is open
## Features
- Keeps your terminal transcript visible while GUI editors are open (no blank Ctrl-G screen)
- Scrollable viewport with mouse wheel and keyboard navigation
- Markdown rendering: headings, bold, inline code, code blocks, lists
- GFM-style table rendering with bounded row/column budgets for predictable performance
- Diff coloring (+green / -red / @@cyan)
- Context usage bar showing token consumption
- OSC-8 hyperlink rendering so long wrapped links remain easy to open
- OSC-8 file/path hyperlink rendering so local paths are easy to open
- Boxed multiline prompt composer is active by default while browsing transcript
- Composer auto-wraps and expands vertically for longer prompts
- File/image path references auto-prepended as `@/absolute/path` when pasted into queue input
- `Ctrl+V` in queue input can attach clipboard files (Finder copy) and clipboard images as `@` references
- Drag-and-drop file paths into queue input are accepted as `@` references
- Terminal resize support (SIGWINCH)
- Works with any GUI editor (TurboDraft, VS Code, Sublime, etc.)
- TurboDraft fast path: talks directly to TurboDraft's Unix socket, bypassing shell overhead and handing off session-scoped queue metadata
- Queue draining is handled by the shipped Claude Stop hook so queued prompts continue automatically
## Requirements
- macOS (arm64 or x86_64)
- A C compiler (Xcode Command Line Tools: `xcode-select --install`)
- `jq` (installed automatically via Homebrew if missing)
## Build from source (manual)
```sh
git clone https://github.com/gradigit/claude-pager.git
cd claude-pager/bin
make
```
This produces `bin/claude-pager-open` (~70KB, zero dependencies).
## Setup
The installer handles everything automatically. If you installed manually:
### 1. Set the editor in settings.json
Add to `~/.claude/settings.json`:
```json
{
"editor": "/path/to/claude-pager-open",
"env": {
"CLAUDE_PAGER_EDITOR": "code --wait",
"CLAUDE_PAGER_EDITOR_TYPE": "gui"
}
}
```
Claude Code sets `editor` as the binary it spawns on Ctrl-G. Since `env` values may not be exported to the editor process, claude-pager reads `~/.claude/settings.json` directly for `env.CLAUDE_PAGER_EDITOR` and `env.CLAUDE_PAGER_EDITOR_TYPE`.
### 2. Install the required hooks
claude-pager uses two Claude hooks:
- **SessionStart** → remembers the exact transcript for the current terminal session
- **Stop** → drains the next queued prompt from the session queue so prompt queuing continues automatically
Add to `~/.claude/settings.json`:
```json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "/path/to/claude-pager/shim/save-session-transcript.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/path/to/claude-pager/shim/queue-drain-stop.sh",
"timeout": 10
}
]
}
]
}
}
```
Without the SessionStart hook, the pager falls back to the most recent transcript in your project directory. Without the Stop hook, the queued prompt composer UI still appears, but queued prompts will not auto-drain back into Claude after the current response completes.
## Switching Editors
Your editor is stored in `env.CLAUDE_PAGER_EDITOR` in `~/.claude/settings.json`. Change it to switch editors:
```json
{
"env": {
"CLAUDE_PAGER_EDITOR": "cursor --wait",
"CLAUDE_PAGER_EDITOR_TYPE": "gui"
}
}
```
Common values:
| Editor | Value |
| --- | --- |
| VS Code | `code --wait` |
| Cursor | `cursor --wait` |
| Zed | `zed --wait` |
| Sublime Text | `subl --wait` |
| Vim | `vim` |
| Neovim | `nvim` |
The resolution order is: `CLAUDE_PAGER_EDITOR` (env or settings.json) → `VISUAL` → `EDITOR` → system default (`open -W -t`).
TUI editors (vim, nvim, emacs, nano, etc.) are exec'd directly without the pager. GUI editors are forked with the pager running alongside.
You can force the path with `CLAUDE_PAGER_EDITOR_TYPE=tui` or `CLAUDE_PAGER_EDITOR_TYPE=gui` in the `env` section (read from env or settings.json).
## Key Bindings
| Key | Action |
| --- | --- |
| Scroll wheel | Scroll up/down |
| Click / Cmd-click | Open hovered OSC-8 link or file path |
| Shift-drag | Select transcript text while mouse interactions stay enabled |
| Arrow keys (in composer) | Move caret and edit wrapped prompt text |
| Page Up/Down | Scroll one page |
| Home / End (in composer) | Jump caret to start / end |
| Shift+Up / Shift+Down | Cycle queued prompts and load selected one for editing |
| Shift+Enter (in composer) | Insert a newline into the queued prompt |
| Ctrl+D (in input mode) | Remove selected queued prompt |
| Ctrl+V (in input mode) | Attach clipboard file/image as `@/absolute/path` reference |
| Enter (in input mode) | Queue prompt or update the selected queued prompt |
| Esc (in input mode) | Restore stashed draft or clear current input text |
| Mouse / Page Up / Page Down | Browse transcript while input stays active |
| Ctrl+Q | Close the active TurboDraft session immediately on the TurboDraft fast path |
## How It Works
When you press Ctrl-G in Claude Code:
1. Claude Code opens an alt screen and spawns the editor shim
2. The C binary finds your session transcript via a tty-keyed temp file (~0.1ms)
3. If TurboDraft is available: connects to its socket and sends `session.open` (~0.02ms) with `cwd`, protocol version, and session-scoped queue metadata
4. It forks and renders the pager directly in C (~3ms for pre-render, ~5ms for full transcript)
5. Your editor opens the file — the pager is already visible
6. Queued prompts are persisted to a session-scoped queue file while you work in the pager composer
7. The shipped Claude Stop hook drains queued prompts back into Claude after each response completes
8. On `Ctrl+Q` in the TurboDraft fast path: the pager requests `turbodraft.session.close` for the active session and waits for `turbodraft.session.wait`
9. On close: once the session actually closes, the binary kills the pager and returns control to Claude Code
The pager keeps mouse interactions enabled for scroll-wheel browsing, link activation, and Shift-drag text selection.
## Architecture
```
claude-pager-open (C binary, ~70KB)
├── Editor resolution (CLAUDE_PAGER_EDITOR from env/settings.json → VISUAL → EDITOR → system default)
├── TUI detection (known TUI list + optional CLAUDE_PAGER_EDITOR_TYPE override + optimistic unknown-editor probe)
├── TurboDraft socket client (JSON-RPC 2.0 over Unix domain socket)
├── Generic editor path (fork editor + fork pager + waitpid)
├── Transcript parser (minimal JSON scanner, single-pass JSONL)
├── Markdown renderer (ANSI escape codes)
├── Scrollable viewport (raw terminal mode, keyboard/mouse input)
└── Recursion guard (_CLAUDE_PAGER_ACTIVE env var)
```
## Agent Instructions
This section is for AI coding agents (Claude Code, Cursor, Copilot, etc.). Everything here is deterministic — no user input is needed.
### 1. Check prerequisites
```sh
# macOS required; ensure a C compiler is available
xcode-select -p >/dev/null 2>&1 || xcode-select --install
# jq required for settings.json manipulation
command -v jq >/dev/null 2>&1 || brew install jq
```
### 2. Clone and build
```sh
git clone https://github.com/gradigit/claude-pager.git ~/.claude-pager
make -C ~/.claude-pager/bin
# verify binary exists
test -x ~/.claude-pager/bin/claude-pager-open
```
### 3. Configure settings.json
Read `~/.claude/settings.json` (create with `{}` if missing). Use `jq` to:
1. Save the current `editor` value as `env.CLAUDE_PAGER_EDITOR` (if it exists and isn't already claude-pager)
2. Set `editor` to the binary path
3. Infer `env.CLAUDE_PAGER_EDITOR_TYPE` (`tui` or `gui`)
4. Add the SessionStart + Stop hooks
Important: Claude hooks must use wrapped hook-group objects with nested `hooks` arrays. Do not write legacy flat command objects directly under `hooks.SessionStart` or `hooks.Stop`.
```sh
BINARY="$HOME/.claude-pager/bin/claude-pager-open"
HOOK_SESSION="$HOME/.claude-pager/shim/save-session-transcript.sh"
STOP_HOOK="$HOME/.claude-pager/shim/queue-drain-stop.sh"
SETTINGS="$HOME/.claude/settings.json"
mkdir -p "$(dirname "$SETTINGS")"
[[ -f "$SETTINGS" ]] || echo '{}' > "$SETTINGS"
# Preserve old editor
OLD=$(jq -r '.editor // empty' "$SETTINGS")
if [[ -n "$OLD" && "$OLD" != *"claude-pager"* ]]; then
jq --arg ed "$OLD" '.env.CLAUDE_PAGER_EDITOR = $ed' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
fi
# If no editor was preserved and none detected, find one
if [[ -z "$(jq -r '.env.CLAUDE_PAGER_EDITOR // empty' "$SETTINGS")" ]]; then
for cmd in cursor code zed subl; do
if command -v "$cmd" &>/dev/null; then
jq --arg ed "$cmd --wait" '.env.CLAUDE_PAGER_EDITOR = $ed' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
break
fi
done
fi
# Set editor to claude-pager-open
jq --arg bin "$BINARY" '.editor = $bin' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
# Infer editor type
tok="$(jq -r '.env.CLAUDE_PAGER_EDITOR // empty' "$SETTINGS" | awk '{print $1}' | xargs basename 2>/dev/null || true)"
case "$tok" in
vi|vim|nvim|lvim|nvi|vim.basic|vim.tiny|vim.nox|vim.gtk|vim.gtk3|emacs|nano|micro|helix|hx|kakoune|kak|joe|ed|ne|mg|jed|tilde|dte|mcedit|amp) ty="tui" ;;
*) ty="gui" ;;
esac
if [[ -n "$tok" ]]; then
jq --arg ty "$ty" '.env.CLAUDE_PAGER_EDITOR_TYPE = $ty' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
fi
# Normalize legacy flat hook objects into Claude's current matcher+hooks schema
jq '
def normalize_event_array:
if type == "array" then
map(
if (type == "object" and (.hooks? | type) == "array") then
.
elif (type == "object" and .type == "command" and (.command? | type) == "string") then
{hooks: [(
if has("timeout") then
{type, command, timeout}
else
{type, command}
end
)]}
else
.
end
)
else
[]
end;
.hooks = (if (.hooks | type) == "object" then .hooks else {} end) |
.hooks.SessionStart = ((.hooks.SessionStart // []) | normalize_event_array) |
.hooks.Stop = ((.hooks.Stop // []) | normalize_event_array)
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
# Add SessionStart hook (if not already present)
if ! jq -e --arg cmd "$HOOK_SESSION" '.hooks.SessionStart[]?.hooks[]? | select(.command == $cmd)' "$SETTINGS" &>/dev/null; then
jq --arg cmd "$HOOK_SESSION" '
.hooks.SessionStart = ((.hooks.SessionStart // []) + [{
"hooks": [
{
"type": "command",
"command": $cmd
}
]
}])
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
fi
# Add Stop hook (if not already present)
if ! jq -e --arg cmd "$STOP_HOOK" '.hooks.Stop[]?.hooks[]? | select(.command == $cmd)' "$SETTINGS" &>/dev/null; then
jq --arg cmd "$STOP_HOOK" '
.hooks.Stop = ((.hooks.Stop // []) + [{
"hooks": [
{
"type": "command",
"command": $cmd,
"timeout": 10
}
]
}])
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
fi
# Validate final hook shape
jq -e --arg session_cmd "$HOOK_SESSION" --arg stop_cmd "$STOP_HOOK" '
(.hooks.SessionStart | type) == "array" and
any(.hooks.SessionStart[]?; (.hooks | type) == "array") and
any(.hooks.SessionStart[]?.hooks[]?; (.type == "command") and (.command == $session_cmd)) and
(.hooks.Stop | type) == "array" and
any(.hooks.Stop[]?; (.hooks | type) == "array") and
any(.hooks.Stop[]?.hooks[]?; (.type == "command") and (.command == $stop_cmd) and ((.timeout // 10) == 10))
' "$SETTINGS" >/dev/null
```
### 4. Done
Tell the user to restart Claude Code and press **Ctrl-G**. The pager will render their conversation transcript while the editor is open.
### Agent notes
- The binary auto-detects TurboDraft's Unix socket — no special config needed for TurboDraft users
- Editor resolution: `CLAUDE_PAGER_EDITOR` (env or settings.json) → `VISUAL` → `EDITOR` → `open -W -t` (macOS default)
- `CLAUDE_PAGER_EDITOR_TYPE` is also read from env or settings.json (`tui`/`gui` override)
- The SessionStart hook enables multi-session transcript lookup; without it the pager falls back to the most recent transcript in the project directory
- The Stop hook drains queued prompts from the session queue back into Claude after each completed response
- `_CLAUDE_PAGER_ACTIVE` env var is set internally to prevent recursion — agents do not need to set this
- No shell config changes (VISUAL/EDITOR) are needed — settings.json is the canonical configuration path
## Development
```sh
git clone https://github.com/gradigit/claude-pager.git
cd claude-pager/bin
make # builds claude-pager-open
make clean # removes build artifacts
```
The C source is in `bin/claude-pager-open.c` (editor resolution + socket + fork logic) and `bin/pager.c` (pager rendering).
The runtime is fully C-based: `bin/claude-pager-open.c` handles editor/session orchestration and `bin/pager.c` handles rendering.
## License
MIT