Skip to content

gradigit/claude-pager

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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)
Before: raw, hard-to-click links and file paths in terminal output After: shortened clickable OSC-8 links and file paths in 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

claude-pager v2 overview with transcript rendering, built-in prompt composer, queue editing, clickable links, and TurboDraft fast path

  • 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

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 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-<version>-macos-arm64.tar.gz (Apple Silicon)
  • claude-pager-<version>-macos-x86_64.tar.gz (Intel)
  • checksums.txt

Then verify:

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 (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)

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:

{
  "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:

{
  "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:

{
  "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) → VISUALEDITOR → 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

# 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

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.

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) → VISUALEDITORopen -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

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

About

Fast C transcript pager for Claude Code: no blank Ctrl-G screen, clickable links/files, queued prompt composer, TurboDraft fast path.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors