YoungCan-Wang 06c95936c3 feat(viewer): explain prompt cache misses in the turn detail (#437)
* feat(viewer): explain prompt cache misses in the turn detail

A turn that misses the prompt cache shows Cache Read 0 with a large
Cache Create, but the token row alone cannot say why: a cold start and a
turn that idled past the 5-minute cache TTL look identical. Reviewers had
to cross-check timestamps by hand to tell them apart.

Add a diagnostic card above the detail sections that names the reason.
Cold start reads "Initial prompt cache creation"; a gap longer than the
TTL reads "Cache expired (idle longer than 5 min)". Turns that extend the
cache normally render no card, so the card's presence is itself a signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(viewer): cover the cache diagnostic card in the contract corpus

Viewer CSS and JS coverage are measured by sweeping the entries produced by
_contract_cases(), and none of those cases reported a cold cache write. The
diagnostic card therefore never rendered during the sweep, so all five of its
new selectors counted as unexercised and viewer_css_diff came out at 0%.

Add a cache_diagnostic_card case with three cold-write entries covering the
initial, TTL-expired, and unidentifiable-cause paths. The last one renders the
low-confidence variant, so .cache-diag-card.low-confidence is exercised too.
_cache_diag_record moves above _contract_cases() because the parametrize
decorator calls it at import time.

viewer_css_diff: 0% -> 100% (5/5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(viewer): only claim a cache miss cause the payloads support

Anthropic writes a small delta to the cache on nearly every turn, so
cache_creation > 0 was not the evidence the diagnosis treated it as: only
a write with no accompanying read means the prefix was genuinely cold.

Follow the prompt hash chain when attributing the miss.  The prompt is
hashed as an ordered chain -- tools, then system, then messages -- so
cache_read == 0 proves the first segment missed and an edit in a later
segment cannot be the cause.  Compare only the region a breakpoint
actually covers, so appending a message beyond the last breakpoint stays
the normal path instead of being reported as an invalidating edit, and
name the earliest changed segment rather than a later one that would send
the reader looking in the wrong place.

Read the TTL tier from the request's cache_control, falling back to the
response's cache_creation tier, and return 0 when neither names one.  The
previous response-only read made the 5-minute default the normal path and
turned unknown lifetimes into confident expiry claims.

Pick the predecessor from the unfiltered history: the sidebar filters are
a viewing choice and must not change what caused a miss.  Candidates have
to share the model and conversation and have taken part in caching
themselves, and a shared session confirms a predecessor whose earlier
messages were rewritten -- which the message-prefix test alone rejects.

Sweep renderCacheDiagnostic while the full corpus is loaded in the
coverage harness; live mode narrows entries to two websocket records
before the detail loop, so no cache-bearing entry was ever rendered.

* fix(viewer): address Codex review on predecessor selection and full content normalization

* fix(viewer): address follow-up review on path-encoded models, structural exactness, and non-cold guard

* fix(viewer): support Bedrock Converse cache points and prefix-bounded diffs

* fix(viewer): wire up the cache prefix bounds and rank expiry first

The previous round added consumers for `prevScopes.toolCount`,
`prevScopes.systemCount`, `prevScopes.bps` and `bp.blockIndex`, but
nothing ever produced them, so every prefix bound degraded to
`Infinity`/`undefined` and the comparisons still spanned whole scopes.

- `cachedScopes` now reports `toolCount`, `systemCount` and the
  breakpoint list, so tool and system comparison stop at the cached
  breakpoint instead of running to the end of the scope.
- `cacheBreakpoints` records `blockIndex` and recognizes Bedrock
  Converse `cachePoint` markers, which cache what precedes them.
- Without any breakpoint the extent of each segment is unknown, so no
  scope is claimed as cached.
- Expiry is evaluated before the structural verdicts and requires an
  exact predecessor, so an edit made after the lifetime elapsed is no
  longer named as the cause.
- A missing predecessor only reads as the initial turn when the trace
  shows the conversation starting.
- In remote dashboard mode the card renders a pending state and upgrades
  once the predecessor payload loads.

* fix(viewer): close four cache-diagnosis gaps from review

Read Converse tool specs from their own list.  `getRequestTools` is a
display helper: it deduplicates by name, merges in tools observed only in
responses, drops the standalone `cachePoint` marker, and never looks at
`body.toolConfig`.  A cache comparison needs the bytes the request sent in
the order it sent them, and the marker is the only thing declaring how far
the cached tool prefix reaches, so `cacheToolList` reads Converse's nested
list directly and attributes the marker to the spec ahead of it.  Without
it a tool-scoped Converse cache reads as unknown extent and a later history
change gets blamed for a miss the tool edit caused.

Take the longest lifetime either source names.  A turn reusing a 1-hour
prefix while appending a 5-minute tail bills only the 5-minute tokens it
wrote, so reading `cache_creation` alone makes the 1-hour prefix look
expired after six minutes -- suppressing an edit inside a still-live prefix
and reporting expiry instead.

Report a cached prefix that got shorter.  Iterating only the common portion
never compares the dropped tail, so truncation went unreported.  A
predecessor that also declared a breakpoint at the shorter boundary left a
live entry there, so that case is excluded rather than blamed; growing the
prefix is the normal incremental path and is not a cause at all.

Re-ask the exactness question after fetching the predecessor.  A dashboard
stub carries no session header and no message bodies, so the search can
only record `exact: false`; every structural and TTL verdict is gated on
that flag, which left multi-message Claude turns permanently unknown
however complete the fetched payload was.  The stub's positive verdict is
kept as a floor, since the entry being diagnosed may itself be a stub.

* fix(viewer): close five remaining cache-diagnosis review gaps

Compare empty tool lists when a later breakpoint covers that segment, so
adding the first tool or removing the last one is a tool miss. Confirm
predecessors with full normalized messages instead of the 500-character
hash. Continue the existing dashboard walk after an inexact fetched
fallback, replace the pending card when the records API fails, and ignore
marker-only Bedrock cachePoint messages when detecting a conversation start.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(viewer): prefer exact predecessors and avoid blaming later edits

Headerless scans keep walking past an inexact neighbor, a leading
message cache point still covers tools and system, and an unchanged
earlier breakpoint makes a later edit an unknown miss rather than a
confident structural cause.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(viewer): read the model out of nested Vertex paths

The cache-diagnostics model check required `/v1/models/<name>` directly,
but Vertex nests the name under the publisher:

    /v1/projects/p/locations/l/publishers/anthropic/models/claude-opus-4-7:rawPredict

Every Vertex turn therefore reported no model at all, so a model switch
looked like a single cache chain and the cold write collected a structural
or TTL diagnosis it had not earned. Match any `/models/` segment, the way
the backend's `_model_from_path` already does.

* fix(viewer): compare the tool list the request actually sent

`cacheToolList` fell through to `getRequestTools`, which ends in
`uniqueToolsByDisplayName` for display. Two definitions can share a name,
and dropping every one after the first meant editing or removing a later
duplicate inside the cached prefix left `toolsChanged` false -- the cold
write came back unknown, or was blamed on a later segment. The cache is
keyed on the bytes sent, so use `body.tools` verbatim when it is there and
keep `getRequestTools` only for the shapes that hold tools elsewhere.

* fix(viewer): treat a shortened system prefix as a system change

`diffCachedRegion` bounded the system comparison to the shorter of the two
block lists, so a predecessor that cached [A, B] against a request sending
[A] compared only the surviving block, found it equal, and reported no
change -- when the shorter prefix was the very reason the lookup went cold.

The message path already had this branch; mirror it for system blocks, with
the same guard: shrinking is a cause only when the predecessor did not also
declare a breakpoint at the surviving boundary, since such an entry would
still be hit.

* fix(viewer): find the session start in the trace, not the message count

A first cache write is not necessarily a first turn. A session stays under
the provider's minimum cacheable size while its prompt is short, so the
opening turns write nothing and are rightly skipped as predecessors -- but
by the time one turn crosses the threshold its own message list already
holds the whole replayed exchange. Asking only about that count called the
capture mid-session and the card fell back to unknown.

Look for the opening turn among the earlier same-session entries instead: if
the capture saw the session start and no earlier turn established a cache,
there is no unseen predecessor and this is the initial write.

* fix(viewer): treat a shortened tool prefix as a tool change

The cached tool segment can break by getting shorter, exactly as the system
and message segments can. Bounding the comparison to the shorter extent walks
only the surviving specs, so a predecessor that cached [Read, Write] against a
request caching [Read] found every compared spec equal and reported no change --
though the dropped spec is why the lookup went cold.

Shrinking is a cause only when the shorter prefix was not itself an entry: a
predecessor that also declared a breakpoint at the surviving boundary left one
there that this request would still hit.

* fix(viewer): report a breakpoint moved back inside one message

A breakpoint that moves from a message's second content block to its first
truncates the cached prefix inside that message. The bounded comparison walked
only the surviving block, found it equal, and left the miss unexplained.

Same rule as for whole messages: it is a cause unless the predecessor declared a
breakpoint at the surviving block too, which left an entry there this request
would still hit.

* fix(viewer): honour an unchanged earlier message checkpoint

The tool and system segments already declined to name a cause the trace
contradicts; the messages need the same argument one segment further in. When
both sides declared a breakpoint at message A and the edit landed at a later
checkpointed message B, the entry at A should still have matched and produced
reads. Zero reads say it did not, so naming B would state a definite cause the
capture refutes.

diffCachedRegion now reports which message first differs, which is what lets the
diagnosis ask whether anything checkpointed sits ahead of it.

* fix(viewer): strip cache markers only where the protocol puts them

cache_control and cachePoint are metadata at three positions: a tool spec, a
system block, and a message content block. Erasing the names recursively also
erased ordinary payload that happens to share them -- a tool schema property
called cache_control, or the same key inside a cached tool_use input -- so an
edited payload compared equal to its predecessor and the invalidation it caused
fell through to the unknown diagnosis.

The markers still have to go from those three positions, or moving one would
read as a content edit rather than the truncation it is.

* fix(viewer): let the trace prove a first write past an inexact predecessor

findCachePredecessor keeps a headerless cache-bearing neighbor as a fallback
because it cannot rule the turn out. Nothing gated on prevIsExact can speak about
such a candidate, but the absence of evidence in it is not the absence of
evidence: the capture can still hold this session opening with no same-session
turn having cached anything ahead of it, which is what an initial write is.

Without this the diagnosis depended on whether an unrelated conversation
happened to be interleaved in the capture -- merely having one turned a provable
cache_miss_initial into unknown.

* fix(viewer): drop standalone cachePoint elements when comparing content

Bedrock states a cache breakpoint as its own array element rather than as a
property on a content block. Stripping the marker key in place left an empty
`{}` occupying that slot, so relocating an intermediate breakpoint shifted the
placeholder and compared unequal -- reporting a *definite* tool, system or
history edit against two identical prompts.

Where the breakpoints sit is already `cachedScopes`' job; this comparison is
only about content, so marker-only elements are dropped outright. A block that
carries a marker alongside real content keeps its place, since that content
still has to be compared.

The regression test relocates an intermediate marker while a tail breakpoint
holds the cached extent equal on both sides, which is the only shape where just
the placeholder moved -- shortening the extent instead takes an earlier exit in
`diffCachedRegion` and would not reach normalization.

* refactor(viewer): state the cache-miss guard once instead of three times

The tools, system and history branches each carried their own copy of the same
argument: a checkpoint ahead of the change, declared on both sides and itself
unchanged, should have produced cache reads, so zero reads contradict blaming
the later change. Three spellings of one invariant meant every adjustment had
to be made three times and could drift apart.

The branches become a `SEGMENTS` list in prompt-hash order, so "ahead" is just
the slice before the changed entry. Messages additionally carry `insideHit`,
the same test applied *within* a segment at block granularity -- tools and
system are compared whole, so a change there has nothing of its own ahead of
it and gets no such test.

Behaviour is unchanged: the tools branch still names directly, system still
defers to an unchanged declared tools checkpoint, and history still defers to
either of those or to a shared message breakpoint before the edit.

* refactor(viewer): only name a cache miss when both sides cached the same extent

The diagnosis used to attribute a miss whenever the cached region got
shorter -- a tool prefix that lost entries, a system prefix that lost
entries, a breakpoint that moved back inside a message.  Naming a cause
there requires knowing which of several declared breakpoints the provider
actually matched, and the trace does not record that.  A request may
declare four breakpoints and hit on any of them; a shorter extent is
consistent with an edit, with expiry, and with eviction alike.

Compare the two extents instead, and only single out a cause when they
agree.  When they differ -- by message count, by segment bound, or by
which block of a message carries the marker -- report
`cache_miss_unknown`, which already reads as a description of the
evidence rather than a finding.  Edits inside an identically bounded
region are still named, so the confident verdicts that were actually
supported survive.

Two cleanups fell out of the review: `cachedScopes` wrote an
`extentUnknown` field nothing read (dead before this branch too), and its
`usage` parameter went unused once that left; and four sites hand-built
the same verdict literal, now routed through `cacheDiagnosticUnknown()`
and `cacheDiagnosticNamed()`.

Verified: embedded JS unit block exits clean, 38 viewer tests pass, ruff
check and format clean, and all four coverage gates pass (viewer_js_diff
86.11%).

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 12:14:33 +08:00
2026-02-15 20:52:11 +08:00

claude-tap

PyPI version PyPI downloads Python version License GitHub stars All Contributors

中文文档

claude-tap is a local proxy and trace viewer for AI coding agents. Run your CLI through it, or listen to local app transcripts, then inspect the real API traffic and agent context: system prompts, conversation history, tool schemas, tool calls, streaming responses, token usage, and request diffs.

Website: Local AI Agent Trace Viewer · Guide: How to view agent traces locally

It works with Claude Code, Codex CLI, Codex App, Gemini CLI, Grok Build CLI, Kimi CLI, MiMo Code, OpenCode, OpenClaw, Pi, Hermes Agent, Cursor CLI, Qoder CLI, Antigravity CLI, and CodeBuddy CLI.

claude-tap demo showing a real Codex trace
Open a real agent run, inspect every request, and compare how context changes between turns.

Light mode trace viewer
Light viewer overview
Dark mode trace viewer
Dark mode for long review sessions
Structured diff modal
Structured diff across adjacent requests

Built with claude-tap

Phistory archives versioned system prompt snapshots from agent CLIs such as Claude Code, Codex, Kimi, opencode, and Pi. It uses claude-tap's capture-only prompt export to preserve raw HTTP trace evidence and generate comparison-friendly prompt snapshots.

Open the prompt diff viewer · View repository
Phistory prompt diff viewer

Why use it

  • 👀 See the exact context: inspect prompts, messages, tool definitions, tool calls, tool results, reconstructed streaming responses, and token usage.
  • 🔎 Debug behavior with evidence: compare adjacent requests and pinpoint which prompt, message, tool, or parameter changed.
  • 📦 Share one portable artifact: each run writes a local trace session that can be exported to a self-contained HTML viewer for review or archiving.
  • 🔒 Keep traces on your machine: no hosted dashboard is required, and common auth headers are redacted before recording.
  • 🧩 Use one workflow across clients: trace Claude Code, Codex CLI, Codex App, Gemini CLI, Grok Build CLI, DeepSeek Harness, Kimi CLI, MiMo Code, OpenCode, OpenClaw, Pi, Hermes Agent, Cursor CLI, Qoder CLI, and CodeBuddy.

Supported Clients

Client Typical use
Claude Code Anthropic API, AWS Bedrock, Claude-compatible gateways such as DeepSeek / GLM, or local proxy upstreams such as CC Switch
Codex CLI OpenAI API key mode or ChatGPT subscription OAuth
Codex App Desktop app launched through forward proxy mode so backend HTTP/WebSocket request bodies are captured
Gemini CLI Google OAuth / Code Assist traffic
Grok Build CLI Grok subscription OAuth sessions through the official CLI chat proxy
DeepSeek Harness dsh headless tasks and custom profiles using DeepSeek or compatible gateways
Kimi CLI Legacy kimi-cli and the newer Kimi Code CLI
MiMo Code MiMo Code sessions (OpenCode fork with multi-provider support)
OpenCode Multi-provider OpenCode sessions
OpenClaw Multi-provider OpenClaw sessions
Pi Pi sessions, including OpenAI Codex OAuth providers
Hermes Agent Multi-provider Hermes TUI or gateway sessions
Cursor CLI / IDE Agent Launch cursor-agent + live transcript watch (claude-tap --tap-client cursor)
Qoder CLI Qoder Agent sessions through forward proxy mode
Antigravity CLI Antigravity Agent sessions through forward proxy mode
CodeBuddy CLI Tencent CodeBuddy SaaS or internal Copilot endpoint

Install

Requires Python 3.11+ and the client you want to trace.

# Recommended
uv tool install claude-tap

# Or with pip
pip install claude-tap

Upgrade: claude-tap update, uv tool upgrade claude-tap, or pip install --upgrade claude-tap

Quick Start

Run the client you want to inspect through claude-tap. Flags after -- are passed to the selected client.

# Claude Code with the live browser viewer enabled by default
claude-tap

# Restore pre-v0.1.75 behavior: no live viewer server
claude-tap --tap-no-live

# Codex CLI
claude-tap --tap-client codex

# Codex App backend request capture
claude-tap --tap-client codexapp

# Gemini CLI
claude-tap --tap-client gemini -- -p "hello"

# Grok Build CLI
claude-tap --tap-client grok -- -p "hello"

# DeepSeek Harness headless task
claude-tap --tap-client dsh -- --profile headless "Reply OK"

# Kimi CLI
claude-tap --tap-client kimi

# New Kimi Code CLI
claude-tap --tap-client kimi-code

# MiMo Code (OpenCode fork)
claude-tap --tap-client mimo

# Pi
claude-tap --tap-client pi -- --model openai-codex/gpt-5.3-codex-spark -p "hello"

# Cursor: launch cursor-agent + live transcript watch + dashboard
claude-tap --tap-client cursor

# Watch IDE Agent transcripts only (no CLI launch)
claude-tap --tap-client cursor --tap-no-launch

# Qoder CLI
claude-tap --tap-client qoder -- -p "hello" --permission-mode dont_ask

# Antigravity CLI
claude-tap --tap-client agy

# CodeBuddy CLI
claude-tap --tap-client codebuddy
Claude Code examples
# Pass flags through to Claude Code
claude-tap -- --model claude-opus-4-6
claude-tap -c    # continue last conversation

# Skip all permission prompts (auto-accept tool calls)
claude-tap -- --dangerously-skip-permissions

# Live viewer is on by default; pass Claude flags after --
claude-tap -- --dangerously-skip-permissions --model claude-sonnet-4-6

claude-tap auto-detects custom Claude Code upstreams from ANTHROPIC_BASE_URL, ANTHROPIC_BEDROCK_BASE_URL, or ANTHROPIC_VERTEX_BASE_URL in your environment or Claude settings. Use --tap-target only when you want to override that detected target.

Local proxy upstreams are supported too: if a tool such as CC Switch points Claude Code at a local ANTHROPIC_BASE_URL, claude-tap detects that value from Claude settings and records the traffic before forwarding it upstream. Use claude-tap in place of claude, such as claude-tap -- <claude-args>; no separate --tap-client value is needed.

For the Claude Code VS Code extension, set Claude Code: Claude Process Wrapper to claude-tap; on Windows, use the full claude-tap.exe path if VS Code cannot find it.

Claude Code with DeepSeek API

Full English guide: Claude Code with DeepSeek API. Simplified Chinese version: Claude Code 搭配 DeepSeek API.

export ANTHROPIC_AUTH_TOKEN="<your DeepSeek API key>"
unset ANTHROPIC_API_KEY

export ANTHROPIC_MODEL="deepseek-v4-pro[1m]"
export ANTHROPIC_DEFAULT_OPUS_MODEL="deepseek-v4-pro[1m]"
export ANTHROPIC_DEFAULT_SONNET_MODEL="deepseek-v4-pro[1m]"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="deepseek-v4-flash"
export CLAUDE_CODE_SUBAGENT_MODEL="deepseek-v4-flash"
export CLAUDE_CODE_EFFORT_LEVEL=max
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
claude-tap -- --permission-mode bypassPermissions

claude-tap reads the DeepSeek upstream from ANTHROPIC_BASE_URL, then launches Claude Code against the local proxy. Use --tap-target https://api.deepseek.com/anthropic only as a manual override.

Claude Code with AWS Bedrock

claude-tap supports three Bedrock scenarios and auto-detects which applies:

Anthropic-compatible Bedrock gateway (New API or similar, no SigV4 in Claude Code)

export ANTHROPIC_AUTH_TOKEN="<your gateway token>"
unset ANTHROPIC_API_KEY
export ANTHROPIC_BASE_URL="https://new-api.example.com"
export ANTHROPIC_MODEL="bedrock/claude-opus-4-6"
export ANTHROPIC_DEFAULT_OPUS_MODEL="bedrock/claude-opus-4-6"
export ANTHROPIC_DEFAULT_SONNET_MODEL="bedrock/claude-opus-4-6"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="bedrock/claude-opus-4-6"
claude-tap -- --model bedrock/claude-opus-4-6

claude-tap records the normal Claude Code /v1/messages HTTP/SSE traffic, then forwards it to the gateway. For model names prefixed with bedrock/, it removes Claude Code beta-only request options that AWS Bedrock rejects while preserving the captured trace.

Custom Bedrock gateway (company proxy, no SigV4)

export CLAUDE_CODE_USE_BEDROCK=1
export ANTHROPIC_BEDROCK_BASE_URL="https://your-gateway.company.com/bedrock"
claude-tap

claude-tap detects the non-AWS host, redirects both ANTHROPIC_BASE_URL and ANTHROPIC_BEDROCK_BASE_URL to the local proxy, and decodes the AWS EventStream binary response format to extract token usage and model info.

AWS native Bedrock (SigV4-signed requests)

export CLAUDE_CODE_USE_BEDROCK=1
export ANTHROPIC_BEDROCK_BASE_URL="https://bedrock-runtime.us-east-1.amazonaws.com"
export AWS_REGION="us-east-1"
claude-tap --tap-proxy-mode forward

When the endpoint is a real AWS domain (*.amazonaws.com), claude-tap does not rewrite ANTHROPIC_BEDROCK_BASE_URL to localhost — doing so would break AWS SigV4 signature validation. Use forward proxy mode (--tap-proxy-mode forward) to capture this traffic without modifying the signed request.

Use --tap-target only as a manual override when auto-detection does not apply.

Claude Code with Google Vertex AI

claude-tap supports Claude Code Vertex pass-through gateways that expose the Vertex rawPredict, streamRawPredict, and count-tokens:rawPredict paths.

export CLAUDE_CODE_USE_VERTEX=1
export CLOUD_ML_REGION="us-east5"
export ANTHROPIC_VERTEX_PROJECT_ID="your-project-id"
export ANTHROPIC_VERTEX_BASE_URL="https://your-gateway.company.com/vertex"
export CLAUDE_CODE_SKIP_VERTEX_AUTH=1  # when your gateway handles auth
claude-tap

When CLAUDE_CODE_USE_VERTEX=1 and ANTHROPIC_VERTEX_BASE_URL is set, claude-tap detects that upstream, redirects both ANTHROPIC_BASE_URL and ANTHROPIC_VERTEX_BASE_URL to the local proxy, and records Vertex rawPredict HTTP/SSE traffic. If Claude Code uses native Google Vertex without ANTHROPIC_VERTEX_BASE_URL, use forward proxy mode or set the base URL explicitly so reverse mode has a single target to forward to.

Codex CLI auth modes and examples

Codex CLI supports two authentication modes with different upstream targets:

Auth Mode How to authenticate Upstream target Notes
OAuth (ChatGPT subscription) codex login https://chatgpt.com/backend-api/codex Default for ChatGPT Plus/Pro/Team users
API Key Set OPENAI_API_KEY https://api.openai.com (default) Pay-per-use via OpenAI Platform

claude-tap auto-detects the Codex target from your auth state when possible. In the default reverse-proxy mode, it launches Codex with a temporary sibling provider whose supports_websockets setting is disabled. This produces one HTTP/SSE trace record per request with the complete request context and does not modify ~/.codex/config.toml.

# OAuth users (ChatGPT Plus/Pro/Team) — auto-detected after `codex login`
claude-tap --tap-client codex

# If auto-detection cannot read your Codex auth file, specify the target explicitly
claude-tap --tap-client codex --tap-target https://chatgpt.com/backend-api/codex

# API Key users — default OpenAI API target works out of the box
claude-tap --tap-client codex

# With specific model
claude-tap --tap-client codex -- --model codex-mini-latest

# Full auto-approval (skip all permission prompts)
claude-tap --tap-client codex -- --full-auto

# OAuth + full auto; live viewer is enabled by default
claude-tap --tap-client codex -- --full-auto
Codex App backend capture examples

Codex App is launched through claude-tap's forward proxy so the final /backend-api/codex/responses HTTP and WebSocket request bodies can be captured in the same trace viewer as other clients. Current macOS installs ship as ChatGPT.app (bundle id com.openai.codex); older standalone Codex.app installs are still recognized. Non-model product traffic is relayed but not persisted as trace rows. On macOS, claude-tap trusts its local CA in the current user's login keychain when needed so the bundled app-server can connect through the proxy.

# Launch Codex App (ChatGPT.app or Codex.app) and inspect captured backend requests
claude-tap --tap-client codexapp

# Keep raw WebSocket/SSE event arrays in the trace
claude-tap --tap-client codexapp --tap-store-stream-events

# Override the executable when the app is installed outside the default locations
CODEX_APP_EXECUTABLE=/path/to/ChatGPT.app/Contents/MacOS/ChatGPT claude-tap --tap-client codexapp

If Codex/ChatGPT App is already running, claude-tap launches an isolated second instance with a dedicated --user-data-dir under ~/.claude-tap/codex-app-profiles/tap so your current window keeps working. You may need to sign in again in the tapped window. Override the profile with CODEX_APP_USER_DATA_DIR. This mode records live backend traffic instead of importing local session JSONL transcripts.

Kimi CLI examples

Use --tap-client kimi for legacy kimi-cli, or --tap-client kimi-code for the newer Kimi Code CLI. Both use reverse proxy mode by default.

claude-tap --tap-client kimi
claude-tap --tap-client kimi -- --thinking
claude-tap --tap-client kimi --tap-target https://api.moonshot.ai/v1

claude-tap --tap-client kimi-code
claude-tap --tap-client kimi-code -- --thinking
claude-tap --tap-client kimi-code --tap-target https://api.moonshot.ai/v1
Gemini CLI examples

Gemini CLI uses forward proxy mode by default. Google OAuth / Code Assist traffic goes to several Google endpoints, so forward proxy capture is the safest default. Reverse mode remains available for API-key or Vertex-style flows that honor GOOGLE_GEMINI_BASE_URL or GOOGLE_VERTEX_BASE_URL.

# Google OAuth / Code Assist
claude-tap --tap-client gemini -- -p "hello"

# Live viewer is enabled by default
claude-tap --tap-client gemini -- -p "hello"

# Reverse mode for compatible API-key / Vertex flows
claude-tap --tap-client gemini --tap-proxy-mode reverse -- -p "hello"
OpenCode examples

OpenCode is a multi-provider terminal AI assistant. Because it can talk to many providers, claude-tap defaults to forward proxy mode for opencode: it injects HTTPS_PROXY plus the local CA into the child process so traffic to any provider is captured.

# Forward proxy mode — captures every provider opencode talks to (default)
claude-tap --tap-client opencode

# Live viewer is enabled by default
claude-tap --tap-client opencode

# Reverse mode — only works when using Anthropic provider (single ANTHROPIC_BASE_URL)
claude-tap --tap-client opencode --tap-proxy-mode reverse
MiMo Code examples

MiMo Code is an OpenCode fork with persistent memory, subagent orchestration, and Xiaomi MiMo platform integration. claude-tap defaults to forward proxy mode for mimocode: it injects HTTPS_PROXY plus the local CA into the child process so traffic to any provider is captured.

# Forward proxy mode — captures every provider MiMo Code talks to (default)
claude-tap --tap-client mimo

# Live viewer is enabled by default
claude-tap --tap-client mimo

# Reverse mode — single Anthropic provider with mimo-only disabled
claude-tap --tap-client mimo --tap-proxy-mode reverse
Pi examples

Pi is a multi-provider coding agent. claude-tap defaults to forward proxy mode for Pi because Pi can use subscription OAuth providers such as openai-codex and custom API-key providers from its model registry.

# OpenAI Codex OAuth via Pi's openai-codex provider
claude-tap --tap-client pi -- --model openai-codex/gpt-5.3-codex-spark -p "hello"

# Live viewer is enabled by default
claude-tap --tap-client pi -- --model openai-codex/gpt-5.3-codex-spark -p "hello"

# Read-only tool capture
claude-tap --tap-client pi -- --model openai-codex/gpt-5.3-codex-spark --tools bash -p "Run pwd"

Pi stores OAuth credentials in ~/.pi/agent/auth.json after /login. If you keep Pi credentials in another directory, set PI_CODING_AGENT_DIR before launching claude-tap.

Hermes Agent examples

Hermes Agent is a multi-provider Python AI agent (Nous Portal, OpenRouter, NVIDIA NIM, Xiaomi MiMo, GLM, Kimi, MiniMax, Hugging Face, OpenAI, Anthropic, custom). Because it can talk to any of these providers — and httpx / requests both honor HTTPS_PROXY natively — claude-tap defaults to forward proxy mode for hermes: it injects HTTPS_PROXY plus the local CA into the child process so any provider is captured.

# Interactive TUI — the recommended way for local trace capture.
claude-tap --tap-client hermes

# Gateway mode — captures LLM calls triggered by incoming platform messages (Slack, Telegram, etc.).
# Requires a messaging platform configured in ~/.hermes/.env.
# claude-tap auto-rewrites `gateway start` → `gateway run` so the gateway runs in the
# foreground and inherits HTTPS_PROXY; without this, the daemon spawned by systemd/launchd
# would not go through the proxy and no traces would be recorded.
claude-tap --tap-client hermes -- gateway start

# Reverse mode is opt-in and only useful when ~/.hermes is configured with an
# OpenAI-compatible provider that reads OPENAI_BASE_URL.
claude-tap --tap-client hermes --tap-proxy-mode reverse

Note: Gateway mode only produces traces when a configured messaging platform (Slack, Telegram, etc.) delivers a message to the bot. Without an active platform integration, the gateway makes no LLM calls and no traces are recorded.

Cursor CLI / IDE Agent examples

Cursor is transcript-only (neither reverse nor forward proxy): claude-tap launches cursor-agent, watches ~/.cursor/projects/*/agent-transcripts/*.jsonl, and writes one dashboard session per Cursor conversation JSONL. It does not MITM api2.cursor.sh.

# Launch agent + live watch + dashboard (default)
claude-tap --tap-client cursor

# Pass args through to cursor-agent
claude-tap --tap-client cursor -- -p --trust --model auto "hello"

# IDE Agent only (no CLI launch)
claude-tap --tap-client cursor --tap-no-launch

Guides and Integrations

Qoder CLI examples

Qoder CLI talks to multiple Qoder endpoints, so claude-tap defaults to forward proxy mode for --tap-client qoder.

# Browser login, PAT, or job token must be configured before launch.
qodercli login

claude-tap --tap-client qoder -- -p "hello" --permission-mode dont_ask
Antigravity CLI examples

Antigravity CLI talks to multiple Google/Antigravity endpoints, so claude-tap defaults to forward proxy mode for --tap-client agy. Its Code Assist model API also honors CLOUD_CODE_URL; claude-tap injects that automatically so model requests such as /v1internal:streamGenerateContent are captured by the same local proxy.

On macOS, Antigravity may not honor per-process CA environment variables. claude-tap automatically trusts the local CA in your current user's login keychain on first agy launch. This does not use sudo or the System keychain, though macOS may prompt to unlock the login keychain.

claude-tap --tap-client agy --tap-live

# Optional: trust the CA separately before launching a forward-proxy client.
claude-tap trust-ca
Grok Build CLI examples

Grok Build uses reverse proxy mode by default. claude-tap temporarily points the official GROK_CLI_CHAT_PROXY_BASE_URL at the local proxy, captures the OpenAI Responses HTTP/SSE stream plus Grok storage and trace audit requests, and relays them to https://cli-chat-proxy.grok.com/v1 with the existing Grok OAuth session.

# Authenticate once with the official Grok Build CLI.
grok login

# Interactive TUI
claude-tap --tap-client grok

# Headless single turn
claude-tap --tap-client grok -- -p "Reply OK"

# Custom Grok-compatible deployment
GROK_CLI_CHAT_PROXY_BASE_URL=https://grok-gateway.example.com/v1 \
  claude-tap --tap-client grok -- -p "Reply OK"
DeepSeek Harness examples

DeepSeek Harness (dsh) uses forward proxy mode by default. This captures model traffic whether the endpoint comes from DEEPSEEK_BASE_URL or a stored dsh model setting, including loopback gateways normally covered by NO_PROXY. The launcher verifies that Node supports --use-env-proxy, records only Chat Completions traffic, and passes all arguments after -- to dsh unchanged. If that Node capability is unavailable, upgrade Node or use reverse mode with an environment-configured endpoint.

# One-shot headless task
claude-tap --tap-client dsh -- --profile headless "Summarize this repository"

# Custom dsh profile
claude-tap --tap-client dsh -- --profile my-profile

# Reverse mode for deployments configured only through DEEPSEEK_BASE_URL
claude-tap --tap-client dsh --tap-proxy-mode reverse \
  -- --profile headless "Reply OK"
CodeBuddy CLI examples

CodeBuddy uses reverse proxy mode by default. claude-tap auto-detects the upstream from CodeBuddy's own login cache (~/.codebuddy/local_storage/), so iOA / WeChat / Google-Github / Enterprise-Domain login modes all work without any extra flag. When the cache is missing (e.g. before first login), it falls back to https://copilot.tencent.com/v2.

# Auto-detected endpoint (works for all four login modes once logged in)
claude-tap --tap-client codebuddy

# Explicit override (e.g. external SaaS or staging)
claude-tap --tap-client codebuddy --tap-target https://www.codebuddy.ai/v2

# Or via environment variable
CODEBUDDY_BASE_URL=https://www.codebuddy.ai/v2 claude-tap --tap-client codebuddy -- -p "Reply OK"
Viewer, export, and advanced options
# Live viewer runs by default while a client runs
claude-tap

# Disable live viewer for scripts, CI, remote shells, or old behavior
claude-tap --tap-no-live

# Browse saved traces without launching a client
claude-tap dashboard

# Stop the shared dashboard service
claude-tap dashboard stop

# Build a local macOS menu bar app, then double-click it in Finder
claude-tap build-macos-app
open "dist/Claude Tap.app"

# Build an Apple Silicon app that bundles Python and dependencies
claude-tap build-macos-app --self-contained

# Restore Claude/Codex configs if the menu app is force-killed while monitoring
claude-tap monitor-restore

# Regenerate a self-contained HTML viewer from JSONL or compact trace input
claude-tap export .traces/2026-02-28/trace_141557.jsonl -o trace.html

# Export a portable compact trace bundle, then render it later.
# Compact is the default export format.
claude-tap export <session-id> -o trace.ctap.json
claude-tap export trace.ctap.json -o trace.html

# Embed the exported viewer in an iframe with reduced chrome
# trace.html?embed=1&hideHeader=1&hidePath=1&hideHistory=1&hideControls=1&density=compact&theme=light

# Store traces in another directory, or keep fewer sessions
claude-tap --tap-output-dir ./my-traces
claude-tap --tap-max-traces 10

# Start only the proxy for custom setups
claude-tap --tap-no-launch --tap-port 8080

# Disable browser auto-open for live and generated viewers
claude-tap --tap-no-open

In proxy-only mode, start your client in another terminal and point its base URL or proxy settings at the local proxy. Use the client support matrix for exact wiring.

When used as VSCode Claude Code's claudeProcessWrapper, claude-tap honors the Claude binary path passed by the extension.

On macOS, claude-tap build-macos-app creates a local Claude Tap.app bundle. The app runs as a menu bar item with a compact status board, Start Monitor / Stop Monitor controls, and a shortcut to the full dashboard. Start Monitor asks for confirmation, launches local reverse proxies for Claude Code and Codex CLI, then writes temporary base-URL settings into ~/.claude/settings.json and ~/.codex/config.toml so newly opened sessions are captured. Codex custom providers are routed through the selected provider's base_url; Claude Bedrock custom gateways are routed when they do not point at native AWS Bedrock endpoints. Native AWS Bedrock endpoints are left unchanged because reverse-mode URL rewriting would break SigV4 signing. Stop Monitor restores the config files byte-for-byte. If the app is force-killed, run claude-tap monitor-restore to restore configs and clean up monitor processes recorded by the app.

By default the launcher points at the current checkout; pass --installed if claude-tap is installed in the Python environment used to build the app. Pass --self-contained to build an Apple Silicon PyInstaller bundle under Contents/Resources so the app does not depend on a colleague's Python installation. Ad-hoc signed builds may still require the recipient to remove quarantine or approve the app in macOS security settings.

CLI Options

All flags are forwarded to the selected client, except these --tap-* ones:

--tap-client CLIENT      Client to launch/listen to: claude (default), agy, codex, codexapp, dsh, gemini, grok, kimi, kimi-code, mimo, opencode, openclaw, pi, hermes, cursor, qoder, or codebuddy
--tap-target URL         Upstream API URL (default: auto per client)
--tap-live               Start real-time viewer while the client runs (default: on)
--tap-no-live            Disable the real-time viewer server (pre-v0.1.75 behavior)
--tap-live-port PORT     Port for live viewer server (default: auto)
--tap-no-open            Don't auto-open live or generated HTML viewers in a browser
--tap-output-dir DIR     Trace output directory (default: ./.traces)
--tap-port PORT          Proxy port (default: auto)
--tap-host HOST          Bind address (default: 127.0.0.1, or 0.0.0.0 in --tap-no-launch mode)
--tap-no-launch          Only start the proxy, don't launch client
--tap-max-traces N       Max trace sessions to keep (default: 50, 0 = unlimited)
--tap-store-stream-events Persist raw SSE/WebSocket event arrays during capture so viewer/export output can show them (default: off)
--tap-proxy-mode MODE    Proxy mode: reverse or forward (default: reverse for claude/codex/grok/kimi/kimi-code/openclaw/codebuddy, forward for agy/codexapp/dsh/gemini/mimo/opencode/pi/hermes/qoder; cursor is transcript-only and ignores proxy MITM)
--tap-trust-ca           On macOS, explicitly trust the local CA in the user login keychain before launch (agy does this automatically)

Viewer Features

Trace viewer capabilities

The viewer is a single self-contained HTML file (zero external dependencies):

  • Structural diff — compare consecutive requests to see exactly what changed: new/removed messages, system prompt diffs, character-level inline highlighting
  • Path filtering — filter by API endpoint (e.g., /v1/messages only)
  • Model grouping — sidebar groups requests by model, with Claude-family priority ordering
  • Token usage breakdown — input / output / cache read / cache creation
  • Tool inspector — expandable cards with tool name, description, and parameter schema
  • Search — full-text search across messages, tools, prompts, and responses
  • Dark mode — toggle light/dark themes (respects system preference)
  • Iframe embed mode — add query parameters such as embed=1, hideHeader=1, hidePath=1, hideHistory=1, hideControls=1, density=compact, and theme=light|dark
  • Keyboard navigationj/k or arrow keys
  • Copy helpers — one-click copy of request JSON or cURL command
  • i18n — English, 简体中文, 日本語, 한국어, Français, العربية, Deutsch, Русский

Architecture

Architecture

How it works

How it works:

  1. claude-tap starts a reverse or forward proxy and spawns the selected client
  2. Base URL clients are pointed at the reverse proxy; clients without base URL support use proxy/CA environment variables
  3. SSE and WebSocket streams are forwarded as chunks/messages arrive with low proxy overhead
  4. Each request-response pair or WebSocket session is recorded to local trace storage; raw SSE/WebSocket event arrays are omitted by default and must be captured with --tap-store-stream-events if you need them later in viewer/export output
  5. On exit, a self-contained HTML viewer is generated
  6. Live mode is enabled by default and broadcasts updates to the browser via SSE

Key features: 🔒 Common auth headers auto-redacted · Low-overhead streaming · 📦 Self-contained viewer · 🔄 Real-time live mode

Community

Ecosystem

  • Phistory archives versioned system prompt snapshots from agent CLIs such as Claude Code, Codex, Kimi, opencode, and Pi. It uses claude-tap's capture-only prompt export to preserve raw HTTP trace evidence and generate comparison-friendly prompt snapshots.

Star History

Star History Chart

Contributors

Thanks goes to these contributors:

liaohch3
liaohch3

💻 📖 🚧 ⚠️
BKK
BKK

💻
YoungCan-Wang
YoungCan-Wang

💻
0xkrypton
0xkrypton

💻
CYJiang
CYJiang

💻
陈展鹏
陈展鹏

📖
devtalker
devtalker

💻
Yaguang Ding
Yaguang Ding

💻
Sephy
Sephy

💻

Contributing

Contributions are welcome. Start with CONTRIBUTING.md.

License

MIT

S
Description
拦截 Claude Code、Codex CLI 等编程智能体的 API 流量,在本地追踪器中查看。|GitHub 镜像 3.1k · 🍴 266
https://github.com/liaohch3/claude-tap Readme MIT 117 MiB
Languages
Python 84.3%
JavaScript 9.7%
HTML 3.5%
CSS 1.9%
Shell 0.6%