INDIE / MACHINE
BACK TO ARCHIVE
FIG. 02PRODUCT HUNT SERIES2026-09-24

Product Hunt Pick: Opaline, Session Analytics That Upload Your Whole Claude Code Transcript

DATE
2026-09-24
SERIES
Product Hunt
View on GitHub
opalinehq/cli

Opaline is #8 on Product Hunt today with 139 points, pitched as "PostHog for team Claude Code and Codex sessions." A CLI hooks into Claude Code and Codex, uploads your session transcripts to a hosted dashboard, and the dashboard shows token cost, time and skill usage per message across a team. It used to be called Rudel.

The CLI is open source, so I built it, ran it in a sandbox against a mock server that records every request, and looked at what actually leaves the machine. The dashboard I did not run. Everything below is about the CLI and the data it sends.

What it is

The repository, opalinehq/cli, is a TypeScript monorepo: MIT, @opalinehq/cli 0.10.0, Node 20 or newer, built with bun. I tested commit f549c07 (2026-09-19). The README says plainly that the repo holds the CLI only and that it connects to "the Opaline app" at opaline.so. The server is closed; a comment in the CLI's own test helpers says anything asserting real ingest behaviour "belongs in the private API repository."

Product Hunt describes deployment as "cloud-hosted or self-hosted/on-premise." The pricing page puts on-prem in Enterprise, from $85 per member per month plus a $1,000 annual platform fee. The free tier is 3 members and 50,000 agent runs a month.

Flow: run npx opaline@latest, pick repositories, review, confirm. It installs a SessionEnd hook in ~/.claude/settings.json and uploads new sessions when they end, plus the sessions that already exist for the repositories you enabled.

Setup

Windows 11, Node 24.11.1, Git 2.53. No install step for the product itself, but the repo pins bun@1.3.10 and I used 1.4.2.

git clone https://github.com/opalinehq/cli && cd cli
bun install          # 38 packages, 7 s
bun run build        # apps/cli/dist/cli.js, 972 KB
node apps/cli/dist/cli.js --version   # 0.10.0

The upstream suite is 782 tests. On Windows, bun test in apps/cli gives 770 pass, 2 skip, 10 fail. I read each failure. Every one is a Windows or POSIX assumption, or my environment:

A trap if you install bun through npm: the shim is bun.cmd, and four upstream tests fail on it with contains a cmd.exe special character until you put the real bun.exe on PATH. The integration suite's own comment says it runs in a Linux job, so I read the Windows failures as an untested platform, not a broken one.

How I tested it

Nothing here touches my real sessions, ~/.rudel or opaline.so. The harness (ph-tests/opaline/) builds a sandboxed HOME, writes synthetic Claude Code sessions, and points OPALINE_API_BASE at a mock server on 127.0.0.1 that answers the RPC calls and records every request. The runner uses DO_NOT_TRACK=1 throughout, so I did not observe the PostHog analytics path at all.

Two drivers:

The scenarios are in opaline.feature; opaline.test.ts and tui.test.ts implement them. 36 tests, all passing on the final run.

The picker after moving to billing-service and pressing Space. One repository is ON, All repos reads MIX, and the header shows 1 UNSAVED.
The picker after moving to billing-service and pressing Space. One repository is ON, All repos reads MIX, and the header shows 1 UNSAVED.

The review screen: one newly added repository, with Confirm selection and Go back.
The review screen: one newly added repository, with Confirm selection and Go back.

After confirming: billing-service is ON, 1 local session, 1 uploaded.
After confirming: billing-service is ON, 1 local session, 1 uploaded.

These are captures of the real TUI in a sandbox, rendered from the terminal's screen buffer.

What's good about it

What I'd push back on

The filter is 18 patterns, and Claude Code sessions are full of .env files

Before upload the CLI runs a secret filter (internal/secret-filter), generated from the Gitleaks v8.30.1 config: AWS access key IDs, GitHub, GitLab, Anthropic, OpenAI, Stripe, Slack, SendGrid, Twilio SK... keys, npm tokens, Google API keys and PEM private keys. I put one fabricated credential of each shape in a Bash tool result, the way cat .env would show up, and imported the session.

Credential in the sessionSent to the server
AWS access key ID[REDACTED:aws-access-key-id]
GitHub personal access tokenredacted
Anthropic API keyredacted
Stripe live secret keyredacted
Slack bot tokenredacted
PEM private keyredacted
AWS secret access key (40 chars)verbatim
Postgres URL with passwordverbatim
SMTP_PASSWORD=...verbatim
JWT (Supabase service role style)verbatim
Authorization: Bearer ...verbatim
Hugging Face tokenverbatim
Twilio auth token (32 hex)verbatim

Six of six covered shapes were redacted; seven of seven uncovered ones went out as written. That is what the docs say, and it is still the part to read before enabling a repository: "known secret patterns" means this list. I expect one common way secrets reach an agent transcript is a file the agent read, and files hold passwords and connection strings that no vendor-prefix regex can see.

The AWS pair is the concrete example. Two adjacent lines:

aws_access_key_id = [REDACTED:aws-access-key-id]
aws_secret_access_key = kOtSLC0pGdGeKok9SficJCWXcDwGuHh3uU5GRWPt

The ID is masked; the secret that makes it useful is not. The values are fabricated. I generated them with a seeded PRNG (my first generator produced near-constant strings, so I replaced it and reran everything; the results did not change).

Some covered keys still get through, depending on the next character

Several rules end with a required terminator: whitespace, a quote, a backtick, a semicolon, an escaped \n, or end of input. That is how the Gitleaks patterns are written. It means the character after the key matters. I called the same filterKnownSecrets function the CLI uses with key=<secret><trailer>x:

Character after the keyAnthropic keyStripe keyGitHub token
space, newline, ", ;redactedredactedredacted
, ) & . : /sentsentredacted

The GitHub rule has no terminator, so it is unaffected. Quoted keys ("sk-ant-...") and a key at the end of a line are caught, which covers most real occurrences. A key in an unquoted URL query string or an argument list followed by a comma is the case that slips. It's rarer than the missing rules above, and silent.

Two limits on all of this. First, a comment in filter.ts says the API filters again on its side; the API is closed and I could not see what it does, so the hosted result may be better than what leaves your machine. Second, my mock does not advertise the "direct R2 ingest" upload path, so the tests exercise the legacy ingest path. The docs say filtering applies to both, and the hook payload carries filter_version: 5, but I did not push secrets through the hook path or the R2 path.

There are safeguards worth naming, from reading the source: the filter reruns until a pass changes nothing (up to four passes), refuses to upload if redaction would break the transcript's JSON, and stops if it would redact more than 20% of the payload. I did not test those.

The payload is the whole transcript plus where it came from

The ingest body carries the full JSONL content (prompt, tool calls, tool output, replies), and alongside it the absolute local path, the git remote, the branch and the commit SHA:

{
  "source": "claude_code",
  "sessionId": "sess-billing-1",
  "projectPath": "D:\\projects\\...\\code\\billing-service",
  "gitRemote": "github.com/example-org/billing-service",
  "gitBranch": "main",
  "gitSha": "84fc58da93b14044e52297d253c631a1cf2a46c9",
  "organizationId": "org-mock",
  "upload_mode": "hook",
  "content": "[907 chars]",
  "filter_version": 5
}

The absolute path usually includes your OS username. The usage-analytics section of the docs says its events exclude local paths; the ingest request itself is a different channel and does include one. That is documented under "repository metadata" but easy to miss.

Nothing else went to the API in my runs. The full list of endpoints across the terminal suite: sessionUploadStatus (4), authStatus (1), setupStatus (1), repositorySettings/report (3), ingestSession (2). I also ran whoami and doctor against the mock. doctor reported the mock's health response as invalid, which is expected, and its version line comes from registry.npmjs.org (cli-release.ts); I saw the output, not the request.

On Windows, import by file path can't recover the project

The picker reads the working directory from inside the session file and gets it right (the payload above). But opaline import <file> decodes the project from the directory name, and the Claude adapter's decodeProjectPath assumes POSIX. Claude Code on Windows names the directory D--projects-.... Result for a real repository at D:\...\billing-service:

"projectPath": "/D//projects/common/ph/tests/opaline/sandbox/suite/.../code/billing/service"

Hyphens in the repository name became separators and no gitRemote or gitBranch was found. Only the manual import path is affected; hooks receive the working directory from Claude Code, and the picker uses the file's own cwd. I hit it with a single sandbox path, so I can't say how it behaves for every layout.

Smaller things

What I did not test

Verdict

If you want session analytics for a team, the CLI is a careful piece of consent UX: off by default, nothing sent until you confirm, OFF really means OFF, and it leaves your other hooks alone. The engineering around uploads is more thoughtful than most of what I've read in this category.

What you are consenting to is the whole transcript, tool output included, with a secret filter that handles 18 known token shapes. Passwords in a .env, connection strings, the AWS secret next to a redacted key ID, and any key followed by a comma go through unchanged. Run it on repositories where sessions never read secrets, or where you already keep secrets out of the agent's reach, and treat the filter as a backstop. Turn on one repository first and read what it uploaded.

Reproduce it: ph-tests/opaline/ holds opaline.feature, opaline.test.ts, tui.test.ts, lib.ts, secrets.ts, tui.ts and render-shots.ts. Clone the CLI at f549c07 into opaline-cli/, build it, put bun.exe on PATH, then bun test opaline.test.ts tui.test.ts and bun render-shots.ts.

Sources: Opaline on Product Hunt · opaline.so · pricing · opalinehq/cli at f549c07 (README.md, docs/data-handling.md, docs/usage.md, internal/secret-filter/, lib/uploader.ts, commands/import.ts, internal/agent-adapters/adapters/claude-code/index.ts)

PREV
Entropy's ML Graph Learns to Execute: LSTM, Sparse MoE, and a Conditioned U-Net
NEXT
A Bowed String in Entropy's DAW: A Delay Loop That Keeps Singing, and a Bow You Can Move
INDIE MACHINE© 2026
A RUST BUILD LOG. NO MOCKUPS, NO ASSUMED NUMBERS.