@soulerou/opik-claudecode
v0.1.1
Published
Opik observability for Claude Code via SessionEnd hook — auto-reports every session to your Opik server, with project name auto-derived from <gitRepoName>-<branch>.
Maintainers
Readme
opik-claudecode
Opik observability for Claude Code — every session is automatically reported to your Opik server, with the project name auto-derived from
<gitRepoName>-<branch>.
npm install -g @soulerou/opik-claudecode
opik-claudecode # interactive setup, then you're doneAfter setup, every Claude Code session you run anywhere on this machine is reported to Opik on session exit — LLM rounds, tool calls, sidechain (subagent) activity, token usage. You don't change how you use Claude Code; the data just shows up.
This is the Claude Code counterpart to opik-opencode. Same data model, simpler runtime — Claude Code's SessionEnd hook invokes a single zero-dep Python script that reads the transcript and POSTs to the Opik REST API.
Contents
- Why
- Quick start
- What you get in Opik
- How it works
- Configuration
- CLI reference
- Project naming
- Re-uploading old sessions
- Troubleshooting
- Limitations
- Server endpoints used
- Layout
- License
Why
You run Claude Code daily. You'd like to:
- See which sessions consumed how many tokens, in which repo, on which branch
- Replay what tools Claude actually invoked, with what inputs and outputs
- Compare conversations across branches or repos to spot regressions or cost spikes
- Build evals against real session transcripts later
Opik gives you all of that, but Claude Code by itself doesn't ship transcripts to anywhere. This package wires the two together — a single SessionEnd hook + a small Python script — and gets out of your way.
Quick start
1. Install
npm install -g @soulerou/opik-claudecodeOr zero-install via npx (downloads once, cached after):
npx @soulerou/opik-claudecodeYou'll see a one-line banner: Run opik-claudecode to configure. That's the only thing npm install does on its own — no user files are written, no prompts in CI, no surprises.
The package is published under the
@soulerouscope, but the bin command after install is the unscopedopik-claudecode(npm uses thebinfield, which is independent of the package name).
2. Configure
opik-claudecodeThe interactive wizard asks two things:
- Scope —
user(recommended; covers every session on this machine) orproject(only this directory). - Server config — Opik URL/IP+port, workspace, and an optional API key.
It then:
- copies the report script into
~/.opik-claudecode/(or./.opik-claudecode/for project scope) - writes your server config next to it
- merges a
SessionEndhook into~/.claude/settings.json(or./.claude/settings.json), preserving any existing hooks
3. Use Claude Code as usual
cd <any-git-repo>
claude # have a conversation
/exit # SessionEnd fires → script runs → Opik gets the dataThat's it. Your session shows up in the Opik project named <repoName>-<branch>.
To check things are wired correctly:
opik-claudecode status
tail -n 5 ~/.opik-claudecode/log/report.logWhat you get in Opik

Interactive SVG:
docs/diagrams/data-model.html
Each Claude Code session lands as one Trace with structured spans inside it:
🧵 Thread: <sessionId> ← Threads view in Opik
│
📦 claudecode-<first user message> ← Trace (thread_id = sessionId)
│
├─ 🤖 claude-opus-4-7 #1 ← LLM Span (round 1)
│ ├─ Input : "I want to refactor the auth flow…"
│ ├─ Output : "Let me start by reading the current login code…"
│ └─ Tokens : input=8 462, output=312
│
├─ 🔧 tool:Read ← Tool Span
│ ├─ Args : { file_path: "src/auth/login.ts" }
│ └─ Output : "import React from …"
│
├─ 🤖 claude-opus-4-7 #2 ← LLM Span (round 2)
│ └─ Output : "I see the issue. We can simplify by …"
│
├─ 🔧 tool:Edit ← Tool Span
│ ├─ Args : { file_path: "src/auth/login.ts", old_string: …, new_string: … }
│ └─ Output : "Edit successful."
│
└─ 🤖 claude-opus-4-7 #3 ← LLM Span (round 3)
└─ Output : "Done — the refactor is in place."| Claude Code | Opik object |
|---|---|
| One session | One Trace (thread_id = sessionId, name = claudecode-<first user msg>) |
| One assistant turn | One LLM Span (name = <model> #<round>, with token usage) |
| One tool call (Bash, Read, Edit, Task, …) | One Tool Span (name = tool:<name>, with args + output) |
| Sidechain / subagent records | Span tagged sidechain (sibling of the parent trace, not nested) |
Same session data is also accessible at the Threads view (Opik aggregates traces by thread_id), so you can scrub through one Claude Code session as a unified timeline.
How it works

Interactive SVG:
docs/diagrams/runtime-flow.html
┌──────────────────────────────────────────────────────────────┐
│ Claude Code session │
│ │
│ user → assistant → tool calls → assistant → … → /exit │
│ │ │
│ SessionEnd hook │
│ │ │
│ ▼ │
│ python3 report_session.py │
│ │ │
│ reads stdin payload + transcript JSONL │
│ resolves project = <gitRepoName>-<branch> │
│ builds 1 Trace + N LLM spans + N tool spans │
│ │ │
└───────┼──────────────────────────────────────────────────────┘
│
▼ POST /v1/private/traces/batch
▼ POST /v1/private/spans/batch
┌─────────────────┐
│ Opik server │ (Cloud / self-hosted / local)
└─────────────────┘- One-shot, end-of-session — the script only runs when Claude Code's
SessionEndhook fires (i.e. on/exit, session resume, or process termination). It reads the entire transcript attranscript_path, builds the Opik payload, and POSTs in two batched calls. - Idempotent — trace and span IDs are deterministic UUID v7s derived from
session_idand per-record UUIDs, so re-running the script for the same session produces the same identifiers (server-side dedup safe). - Never blocks Claude — telemetry errors are logged to
~/.opik-claudecode/log/report.logand the script always exits 0. - Zero runtime deps — only
python3(3.8+) and Node stdlib. The Python script uses onlyurllib/json/subprocessfrom stdlib.
Configuration
Resolution order (high → low)
- CLI flags —
--api-url,--api-key,--workspace,--project(only when runningreport_session.pyad-hoc) - Env vars —
OPIK_API_URL,OPIK_API_KEY,OPIK_WORKSPACE_NAME,OPIK_PROJECT_NAME - Project-level config —
<repo>/.opik-claudecode/opik-claudecode.json - User-level config —
~/.opik-claudecode/opik-claudecode.json - Defaults —
apiUrl=http://localhost:5173/api,workspaceName=default,projectName=auto
Config file shape
Both user-level and project-level files share the same shape (opik-claudecode.json):
{
"apiUrl": "http://10.101.102.98:80/api",
"apiKey": "",
"workspaceName": "default",
"projectName": null
}| Field | Default | Notes |
|---|---|---|
| apiUrl | http://localhost:5173/api | Opik REST endpoint base. Include /api when self-hosted. |
| apiKey | "" | Leave blank for self-hosted. Set for Opik Cloud. |
| workspaceName | "default" | Sent in the Comet-Workspace header. |
| projectName | null | null = auto-derive <gitRepoName>-<branch>. Set explicitly to override. |
Non-interactive setup
For dotfiles, container images, or provisioning scripts:
OPIK_API_URL=http://10.101.102.98:80/api \
OPIK_WORKSPACE_NAME=default \
OPIK_API_KEY= \
opik-claudecode --user --yes--yes skips the interactive scope picker and uses defaults / env vars for everything.
CLI reference
opik-claudecode [setup] interactive setup (default)
opik-claudecode [setup] --user non-interactive user-scope install
opik-claudecode [setup] --project non-interactive project-scope install
opik-claudecode status [--user|--project]
opik-claudecode uninstall [--user|--project]
opik-claudecode help| Verb | What it does |
|---|---|
| setup (default) | Copy script + write config + merge hook. Idempotent — re-running won't duplicate the hook entry. |
| status | Show install dir, script presence, parsed config, hook wiring — for both scopes by default. |
| uninstall | Remove only our SessionEnd hook entry from settings.json. Preserves all other hooks. Leaves the install dir intact. |
| help | This usage page. |
| Flag | Meaning |
|---|---|
| --user | Skip scope prompt; target ~/.claude/settings.json and ~/.opik-claudecode/. |
| --project | Skip scope prompt; target ./.claude/settings.json and ./.opik-claudecode/. |
| --yes, -y | Non-interactive — use env vars / existing config / defaults. |
| --help, -h | Show usage. |
Project naming
The default project name is derived from your git context, at session-end time:
project_name = "<gitRepoName>-<branch>"| Where | How it's resolved |
|---|---|
| gitRepoName | basename(git rev-parse --show-toplevel) from the session's cwd |
| branch | git rev-parse --abbrev-ref HEAD from cwd (live, authoritative) |
| Fallback | Transcript's recorded gitBranch field — only used if the live call fails |
| Outside a repo | Literal fallback: claudecode |
Slashes in branch names are normalized to - (so feature/foo → feature-foo). Detached HEAD shows up as <repo>-HEAD.
To override: set OPIK_PROJECT_NAME=my-name in your env, or set "projectName": "my-name" in opik-claudecode.json.
Re-uploading old sessions
The report script can be invoked manually for any past session — useful for backfilling, or for testing the wiring without doing a real Claude run:
python3 ~/.opik-claudecode/report_session.py \
--session <session-id> \
--transcript ~/.claude/projects/-Users-…/<id>.jsonl \
--cwd /path/to/the/repo \
--api-url http://10.101.102.98:80/api \
--project my-project--dry-run prints the would-be JSON payload to stdout without posting:
python3 ~/.opik-claudecode/report_session.py \
--session abc --transcript /path/to/transcript.jsonl \
--cwd /tmp/repo --dry-run | jq .trace.nameRe-running for the same session_id is idempotent at the ID level — trace/span IDs are deterministic, so the server sees the same identifiers each time.
Troubleshooting
Nothing appears in Opik after /exit
opik-claudecode status # is the hook wired?
tail -n 30 ~/.opik-claudecode/log/report.logThe log will contain the full sync line (sync session=… project=… spans=… endpoint=…) and any HTTP error from the POST. Common causes:
apiUrltypo — double-check the host/port; for self-hosted Opik the path is…/api, not the UI URL.- Workspace mismatch — Cloud users must set
workspaceName(Cloud rejects requests withoutComet-Workspace). - Network unreachable — log shows
network: <reason>. Check firewalls, VPN, etc.
Hook fires, but the project is named claudecode instead of <repo>-<branch>
The script ran outside a git repo (or from a path where git rev-parse failed). Verify with:
git -C "$PWD" rev-parse --show-toplevel
git -C "$PWD" rev-parse --abbrev-ref HEADIf both succeed but the project still falls back, set OPIK_PROJECT_NAME explicitly or hard-code projectName in the config file.
python3: command not found
The hook command is python3 …. Either install Python 3.8+ and put it on the PATH, or edit the hook in settings.json to point at a specific interpreter (e.g. /opt/homebrew/bin/python3 on macOS).
Sessions on different machines should report to the same Opik project
Set projectName explicitly in opik-claudecode.json (or OPIK_PROJECT_NAME in your shell rc) so all machines agree on the name regardless of their local git layout.
Limitations
- Not streaming. Sessions appear in Opik only after
SessionEnd. If Claude Code crashes hard before exit, that session is not reported. - Cache-token detail not aggregated at trace level. Per-span
metadata.usagecarries the fullcache_creation_input_tokens/cache_read_input_tokensvalues — but trace totals only suminput_tokens + output_tokens. Add up cache fields yourself in the Opik UI if needed. - Sidechain spans aren't nested. Subagent records are tagged
sidechainand emitted as siblings under the parent trace, not nested under a single subagent parent span. Good enough for filtering and grouping. - Light payload sanitization. Each text field is truncated to 16 KiB. There's no keyword-based redaction yet — don't run this against sessions where Claude saw real production secrets unless your Opik instance is trusted.
Server endpoints used
Verified against the Opik REST API docs:
| Path | Method | Body | Used for |
|---|---|---|---|
| /v1/private/traces/batch | POST | {"traces":[…]} | The single session-level trace |
| /v1/private/spans/batch | POST | {"spans":[…]} | LLM + tool spans, batched 100 per request |
Headers (set when configured):
Content-Type: application/json
Authorization: <api-key>
Comet-Workspace: <workspace>Field naming throughout is snake_case (thread_id, start_time, project_name, trace_id).
Layout
opik-claudecode/
├── README.md # this file
├── package.json # npm metadata; bin → bin/opik-claudecode.js
├── install.sh # bash installer (alternative to the npm CLI)
├── bin/
│ ├── opik-claudecode.js # CLI entry — pure Node stdlib
│ └── postinstall-banner.js # one-line note after `npm install` (silent in CI)
├── lib/
│ └── setup.js # setup / status / uninstall logic
├── scripts/
│ └── report_session.py # the script the hook runs (zero-dep, Python 3.8+)
├── templates/
│ ├── settings.hooks.user.template.json # SessionEnd hook fragment (user scope)
│ ├── settings.hooks.project.template.json # SessionEnd hook fragment (project scope)
│ └── opik-claudecode.example.json # config-file example
└── docs/
└── diagrams/
├── runtime-flow.html # dark-themed SVG of the runtime flow
├── runtime-flow.preview.webp # raster preview (embedded in README)
├── data-model.html # dark-themed SVG of the data-model mapping
└── data-model.preview.webp # raster preview (embedded in README)Manual install (no npm, no install.sh)
If you'd rather wire it yourself, copy scripts/report_session.py somewhere stable and add this to ~/.claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "python3 /absolute/path/to/report_session.py",
"timeout": 60
}
]
}
]
}
}The script reads its server config from env vars (OPIK_API_URL, OPIK_API_KEY, OPIK_WORKSPACE_NAME, OPIK_PROJECT_NAME) or from ~/.opik-claudecode/opik-claudecode.json. Pick whichever you prefer.
