npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pinta-ai/pinta-cc

v1.9.0

Published

Generic OTLP forwarder for Claude Code hook events

Readme

pinta-cc — Generic OTLP forwarder for Claude Code hook events

Converts Claude Code hook events into OTLP/HTTP spans and forwards them to any OpenTelemetry-compatible collector. No Pinta CLI dependency. No identity check at plugin time. Vendor-neutral.

Features

  • OTLP transport: converts 11 hook event types into OTLP/HTTP resourceSpans and sends them via POST {endpoint}/traces
  • Bronze flattening: every top-level field of a hook event is flattened into cc.<key> span attributes
  • ULID per-turn traceId: UserPromptSubmit starts a new ULID-based trace; all subsequent hooks in the same turn share it
  • Retry queue: on transport failure, payloads are appended to .plugin-data/failed-spans.jsonl (cap 1000) and flushed on the next hook invocation
  • Vendor-neutral: any OTel-compatible collector works. Pinta Manager auto-configures the endpoint and token
  • Identity at relay: member.identity.* attributes are no longer attached at plugin time. Pinta Manager (or your own pipeline) attaches identity at the forwarding layer

Channels

| Channel | Install path | Auto-update | |---------|-------------|-------------| | Pinta Manager v0.2+ | Manager installs and configures automatically | Yes — on manager reconcile | | Marketplace pinta-cc@pinta-ai | /plugin marketplace add pinta-ai/pinta-cc then /plugin install pinta-cc@pinta-ai | Yes — on Claude Code startup |

Installation

Pinta Manager (recommended for enterprise)

Pinta Manager v0.2+ handles installation and configuration automatically. No manual steps required.

Marketplace

/plugin marketplace add pinta-ai/pinta-cc
/plugin install pinta-cc@pinta-ai

After installation, Claude Code automatically pulls new versions from the marketplace on every startup.

Direct from GitHub

claude plugin install github:pinta-ai/pinta-cc

Local development

git clone https://github.com/pinta-ai/pinta-cc.git
cd pinta-cc
npm install && npm run build
claude --plugin-dir .

Configuration

userConfig (Claude Code plugin settings)

| Setting | Description | Required | |---------|-------------|----------| | endpoint | OTLP/HTTP traces endpoint. Pinta Manager auto-fills. OSS: any OTel collector URL | Yes | | api_key | Token sent as x-pinta-relay-token header. Pinta Manager auto-fills | Conditional |

Pinta Manager scenario

Pinta Manager v0.2+ injects CLAUDE_PLUGIN_OPTION_ENDPOINT and CLAUDE_PLUGIN_OPTION_API_KEY automatically via Claude Code's settings.json. No manual configuration needed.

OSS / direct scenario

Set endpoint to your OTLP collector URL (e.g. http://localhost:4318) and api_key to whatever token your collector expects. The token is sent as x-pinta-relay-token.

For collectors that need a different auth header, set OTEL_EXPORTER_OTLP_HEADERS directly in your environment — this overrides the api_key userConfig:

export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <token>"

OTel env var precedence

The plugin reads OTel env vars directly. CLAUDE_PLUGIN_OPTION_* env vars (set by Claude Code from userConfig) are bridged to their OTEL_EXPORTER_OTLP_* equivalents only when the OTel var is not already set. Explicit OTEL_EXPORTER_OTLP_* always win.

| Plugin userConfig | Bridged to | |------------------|-| | CLAUDE_PLUGIN_OPTION_ENDPOINT | OTEL_EXPORTER_OTLP_ENDPOINT | | CLAUDE_PLUGIN_OPTION_API_KEY | OTEL_EXPORTER_OTLP_HEADERS=x-pinta-relay-token=<key> |

Span attribute conventions

Resource attributes

| Attribute | Value | |-----------|-------| | service.name | "claude-code" | | service.version | Claude Code CLI version | | telemetry.sdk.name | "pinta-cc" | | telemetry.sdk.version | Plugin version | | process.pid | Hook process PID | | process.owner | OS username | | host.name | Machine hostname | | host.arch | CPU architecture |

Note: member.identity.* attributes are not attached at plugin time (moved to relay layer).

Claude Code version discovery

service.version is the Claude Code product version, not the Pinta plugin version. The hook prefers CLAUDE_CODE_EXECPATH, resolves symlinks, and reads Claude's npm package metadata when available. For native installations it runs that executable with --version (no shell, 2-second timeout, bounded output) and accepts only output identifying Claude Code. Without CLAUDE_CODE_EXECPATH, it searches absolute PATH entries, then ~/.local/bin/claude. An invalid explicit path never silently selects a different installed CLI.

Native probe results are cached in claude-version.json under CLAUDE_PLUGIN_DATA (default: ${CLAUDE_PLUGIN_ROOT}/.plugin-data), shared by separate hook processes. The cache holds at most eight executables and invalidates on resolved path or file metadata changes. Successful results expire after 24 hours; failed probes retry after 30 seconds. A cache write failure does not discard a discovered version.

If discovery fails, the hook sends unknown and writes a [pinta-cc] CLI version: diagnostic to stderr without copying child output. Audit Log displays this as an empty Version, rather than presenting a fabricated version. Check the hook's executable path and cache directory permissions when troubleshooting. Old records with missing versions are not backfilled. Version probes are skipped when telemetry is disabled.

Span attributes

| Attribute | Value | |-----------|-------| | ingest.type | "cc" (discriminator for aware-backend parser) | | cc.hook | Hook event name (e.g. PreToolUse) | | cc.model | Exact host-reported scalar model ID, when attributable | | cc.model_source | Host provenance or adapter evidence (see below) | | cc.<key> | All other top-level hook event fields (Bronze flattening) |

Model attribution and its limits

An explicit hook model takes precedence: a string or a descriptor's id (or name when no id field exists), not stringified JSON. The source preserves the host's supplied model_source or defaults to hook.model. In particular, a SessionStart model is a host-selected model, not proof of the routed response model, and it is not cached for later tool calls.

JSON-stringified objects/arrays (including [object Object]) are not IDs. Provider and requested/response fields remain unchanged. If a transcript replaces an unusable model, model_source describes that transcript evidence; a previous host source is retained as cc.model_original_source.

For tool hooks without a usable model, the adapter reads the supplied transcript_path and uses message.model only from an assistant record whose message.content contains the exact tool_use ID from tool_use_id. cc.model_source=transcript.assistant.message identifies this evidence. The record's sessionId must match, its timestamp must not be in the future relative to the hook (or processing time if no hook timestamp is supplied), and its subagent identity must match. The agent is identified by agent_id or a session-bound <session_id>/subagents/agent-<id>.jsonl path, never by agent name. Sidechain/child records cannot supply a parent tool's model.

PreToolUse, PostToolUse, PostToolUseFailure and permission hooks can use this lookup when they carry tool_use_id. Stop/lifecycle hooks without an explicit model, missing or not-yet-flushed assistant records, unsupported transcript formats, ambiguous/conflicting IDs, and records outside the read window remain model-less. There is no "latest assistant" fallback, no session-wide carry-forward, and no inference from global configuration, process/CLI versions, agent names or user prose.

Blank, unknown, undefined, null, n/a, none, auto and default are omitted case-insensitively. Non-ID objects, control characters and IDs longer than 512 characters are also rejected. The input event is not mutated, and all model attributes still use the existing redaction pipeline.

Model lookup is read-only and bounded to a 256 KiB prefix plus a 1 MiB tail and 4,096 complete JSONL records, with no subprocess, directory scan or model cache. Only absolute .jsonl regular files are opened; missing/unreadable files, leaf symlinks, malformed records and invalid identities fail quietly, and incomplete boundary lines are ignored. Transcript contents are never logged or exported. Explicit models and missing tool IDs require no model-lookup IO. Guard behavior and event counts are unchanged.

Architecture

src/
├── index.ts              # Entry point: env-bridge → stdin parse → handler routing
├── core/
│   ├── env-bridge.ts     # CLAUDE_PLUGIN_OPTION_* → OTEL_EXPORTER_OTLP_* alias
│   ├── types.ts          # Hook event types, type guards, skip-list
│   ├── config.ts         # loadConfig() + hasOtlpEndpoint()
│   ├── otlp.ts           # OTLP payload builder + Bronze flattening + ULID→traceId
│   ├── transport.ts      # POST {endpoint}/traces (5s timeout, reads OTel env at call time)
│   ├── retry-queue.ts    # File-based JSONL queue (cap 1000, 30s stale lock TTL)
│   ├── trace.ts          # traceId management (ULID, file-based sharing)
│   ├── identity.ts       # Empty stub (identity attribution moved to relay)
│   └── redact.ts         # Tier-1 redaction + Tier-3 truncation
└── handlers/
    ├── pre-tool-use.ts   # flush → currentTrace → buildOtlpPayload → send → exit 0
    ├── post-tool-use.ts
    ├── user-prompt.ts    # flush → newTrace → buildOtlpPayload → send → exit 0
    ├── session.ts
    ├── subagent.ts
    ├── stop.ts
    ├── permission.ts
    └── default.ts        # Notification, TaskCreated, TaskCompleted → exit 0 immediately

Event flow

UserPromptSubmit (newTrace() → POST /traces)
  → PreToolUse (currentTrace() → POST /traces)
  → PostToolUse (currentTrace() → POST /traces)
  → ...
UserPromptSubmit (next turn, new traceId)
  → ...

Each hook invocation spawns a fresh Node process. One hook = one OTLP span = one resourceSpans entry. Retry queue flush batches multiple spans into a single body.

Captured events (11)

PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, SessionStart, SessionEnd, PermissionRequest, PermissionDenied, SubagentStart, SubagentStop, Stop

Skipped events (3, exit 0 immediately)

Notification, TaskCreated, TaskCompleted

Development

npm install
npm run build         # tsc → dist/
npm test              # vitest run
npm run smoke:version # built CJS/ESM hooks -> local collector (after build)
npm run smoke:model   # model attribution -> isolated loopback collector
npm run mock-server   # Generic OTLP collector at http://localhost:3000

Local integration test

smoke:model exercises fresh built CJS/ESM hooks with isolated HOME/plugin data, no real Claude executable, and no manager/guard calls. Supplied, missing, placeholder, correlated, cross-session/subagent, future, partial and oversized transcript cases are verified. Results go to .validation/model-smoke.json. Optionally pass -- --baseline=<previous-built-index.js> for paired hook wall-time comparisons, including Node startup, local HTTP and scheduler noise.

smoke:version uses isolated plugin data and a loopback collector, not your real configuration. On macOS/Linux it supplies an executable fixture by default. To check a real native installation (required on Windows), run after building:

PINTA_TEST_CLAUDE_BINARY=/absolute/path/to/claude \
PINTA_TEST_CLAUDE_VERSION=2.1.278 \
npm run smoke:version

Set the expected version to the actual product version of that binary. The test launches both built hook entrypoints with stdin payloads and inspects the serialized resource attributes received over HTTP.

# Terminal 1: start the mock OTLP collector
npm run mock-server

# Terminal 2: run Claude Code with the plugin
CLAUDE_PLUGIN_OPTION_ENDPOINT=http://localhost:3000 \
CLAUDE_PLUGIN_OPTION_API_KEY=test-token \
claude --plugin-dir .

Open http://localhost:3000 to inspect captured spans.

BREAKING CHANGES from 1.1.x

| What changed | 1.1.x | 1.2.x | Migration | |---|---|---|---| | Pinta CLI dependency | Required (pinta login) | Removed | Nothing; Pinta Manager or direct OTel env | | endpoint meaning | Pinta backend URL | Any OTLP collector URL | Update to your collector's /v1/traces or equivalent | | api_key meaning | Pinta backend API key (x-api-key header) | Relay token (x-pinta-relay-token header) | Pinta Manager: automatic. Standalone: update token | | Identity attributes | member.identity.id/email in resource attrs | Removed | Attach at your collector/forwarder layer | | PreToolUse fail-close | Exit 2 (deny) when identity unresolved | Removed — always exit 0 | No action needed | | src/enterprise/ | Present | Removed | No action needed |

Pinta Manager users: No action required. Manager v0.2 auto-injects config on next reconcile.

Marketplace users: Auto-updated on next Claude Code startup. Update endpoint userConfig to your OTLP collector URL if you manage your own pipeline.

Direct env var users: Replace CLAUDE_PLUGIN_OPTION_API_KEY token semantics and ensure your collector accepts x-pinta-relay-token, or set OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <token> directly.

License

PolyForm Noncommercial 1.0.0 — see LICENSE.

Commercial use is not permitted under this license. Noncommercial use (personal projects, research, educational institutions, nonprofits, government) is allowed. For a commercial license, please contact Pinta AI.