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-copilot

v0.11.0

Published

Generic OTLP forwarder + guard for GitHub Copilot hook events (CLI + VS Code extension)

Downloads

1,541

Readme

pinta-copilot — OTLP forwarder + guard for GitHub Copilot hooks

Converts GitHub Copilot hook events into OTLP/HTTP spans and forwards them to any OpenTelemetry-compatible collector, with an optional external guard for tool calls and their returned contents. Vendor-neutral. No Pinta CLI dependency. Identity is attached at the relay layer.

A single adapter + a single hook file covers two surfaces:

| Surface | Hook source | Guard | Notes | |---|---|---|---| | Copilot CLI / SDK | ~/.copilot/hooks/pinta-copilot.json | preToolUse, permissionRequest, postToolUse | Denied successful output is replaced before the model receives it: the policy message, a recovery hint, and the output with the detected values masked (or withheld) | | VS Code Local harness (in-editor Copilot Chat) | same ~/.copilot/hooks/ file | PreToolUse, PostToolUse | Denied successful output stops the current run with the policy message (no result-replacement channel) |

Automatic cloud hook installation (.github/hooks/) remains out of scope. When registered there, the output response uses the Copilot CLI/SDK contract. VS Code Agent Host Copilot also uses that contract, not the Local harness one.

Why it works with no VS Code setup

The VS Code Copilot extension reads the same ~/.copilot/hooks/ file the CLI does (VS Code core DEFAULT_HOOK_FILE_PATHS). No VS Code setting is required — in particular chat.useClaudeHooks works at its default false. Install the one file and both the CLI and in-editor Copilot Chat fire it. (Verified against Copilot CLI 1.0.49 + VS Code, 2026-06.)

⚠️ Fail-closed safety

Copilot's CLI preToolUse command errors are fail-closed: a non-zero exit or crash can deny the tool and disrupt internal control tools. Timeouts are a separate host boundary and can fail open. This adapter therefore always exits 0 on every path; transport and guard failures are reported on stderr rather than thrown past the top-level handler. Guard timeout/error behavior remains fail-open.

Install

git clone https://github.com/pinta-ai/pinta-copilot.git
cd pinta-copilot
npm install && npm run build
npm run install-hooks        # writes ~/.copilot/hooks/pinta-copilot.json (absolute paths)

Restart the Copilot CLI / reload the VS Code window to load hooks. Remove with npm run uninstall-hooks.

Managed installs (Pinta Manager) write the same file via the sidecar enroll module — no manual step.

Configuration

Config is read from an env file the adapter loads at startup — ~/.copilot/pinta-copilot.env (or $COPILOT_HOME/pinta-copilot.env), KEY=VALUE per line. Explicit process.env (incl. a hook env block) overrides the file; the file overrides legacy keys.

# ~/.copilot/pinta-copilot.env
COPILOT_PLUGIN_OPTION_ENDPOINT=https://your-collector.example.com/v1/traces
COPILOT_PLUGIN_OPTION_HEADERS=x-pinta-relay-token=YOUR-TOKEN
# optional: external guard (allow/deny tool calls)
PINTA_GUARD_ENDPOINT=https://your-relay.example.com/guard

| Var | Purpose | |---|---| | COPILOT_PLUGIN_OPTION_ENDPOINT | Full OTLP/HTTP traces URL. Namespaced to avoid colliding with Copilot's native OTel (OTEL_EXPORTER_OTLP_*). The standard OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT are honored as a lower-priority fallback. | | COPILOT_PLUGIN_OPTION_HEADERS | key=val,key=val request headers (auth). Falls back to OTEL_EXPORTER_OTLP_HEADERS. | | PINTA_GUARD_ENDPOINT | Optional. POST'd before tool execution and on successful postToolUse. DENY gates the call or its returned output, respectively. | | COPILOT_HOME | Overrides ~/.copilot for hook + env-file paths. |

Guard (allow / deny + reason)

On preToolUse (all surfaces) and permissionRequest (CLI only) the adapter queries PINTA_GUARD_ENDPOINT. A DENY is emitted in the registration's protocol format and the reason is shown to the model/user:

  • Native camelCase preToolUse → { "permissionDecision": "deny", "permissionDecisionReason": "<reason>" }
  • PascalCase PreToolUse → { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "<reason>" } }
  • permissionRequest → { "behavior": "deny", "message": "<reason>" }

Copilot CLI 1.0.88 ignores the nested PascalCase denial on a native camelCase hook. The adapter selects the response from the explicit event discriminator or trusted per-hook PINTA_COPILOT_EVENT binding, not from tool-input contents. Preserve the registered spelling in that binding.

Guard is fail-open (no endpoint / timeout / error → allow), so it never breaks a session.

Once a DENY is decided, the hook does not wait for collector IO. It emits the enforcement response, appends the original redacted span to the existing local retry queue, and finishes. This applies to before-tool, permission, and after-tool denials: otherwise a collector delay can cause the host to time out and discard even an already-written denial. The next eligible telemetry hook flushes the queued span with its original IDs and evidence. Backend visibility can therefore be delayed until that hook; a stopped run may need a new session to trigger delivery. No span is queued when telemetry is disabled. Local persistence failures are reported without discarding the enforcement response.

Successful tool-output enforcement

Before-tool hooks cannot inspect contents that the tool has not returned yet. PostToolUse now sends the original redaction-aware OTLP payload to the same guard. On DENY, the adapter emits the host's supported response and defers network telemetry as described above. What the model receives is built by @pinta-ai/core's buildOutputDenial from the manager's answer (PTA-695):

  • notice — the policy's own userMessage (or a default) plus (evaluation <id>) when the manager sent an evaluationId, so a false positive can be reported by reference.
  • guidance — a fixed recovery hint: for a mask, continue with the masked text and ask the user for any value that is required; for a withhold, do not retry to bypass the block and tell the user what was withheld.
  • output — only when the manager answered outputAction: "mask" with redactions, the original output with each detected value replaced by [REDACTED:<category>] (inside JSON strings the escaped form is masked too). Any other answer — outputAction: "withhold", a value that is not in the output, or an older manager that sends neither field — withholds the output.

Per surface:

  • Copilot CLI/SDK/cloud: modifiedResult with resultType: "success" and textResultForLlm = notice + "\n\n" + guidance (+ "\n\n" + masked output for a mask) replaces the denied result. The session continues.
  • VS Code Local: continue: false with stopReason = notice stops the run. The Local PostToolUse contract has no result-replacement channel (only decision: "block", additionalContext, continue/stopReason, systemMessage), so masked output cannot be delivered there; this is a documented limitation, not a policy choice.

The native tool_result/toolResult shape takes precedence over inherited editor environment variables; Copilot SDK sessions inside VS Code must not receive a Local-only stop response. Local tool_response uses the stop contract. Both snake_case and camelCase input forms are supported; response keys use the documented casing. The original text is read from tool_result.text_result_for_llm / toolResult.textResultForLlm (CLI/SDK) or tool_response (Local); a non-string result is JSON-stringified first.

Keep the generated hook registration's per-command env: { "PINTA_COPILOT_EVENT": "PostToolUse" } binding. Some native Copilot payloads omit every event-name field. The installer and Manager bind each registered event explicitly; custom registrations must do the same, using the matching name for each hook, not one global value for all events. Without either a discriminator or that binding, the event remains unknown and cannot be guarded. Tool-result contents are not used to guess the firing event.

The untrusted output reaches the model only masked, or not at all. The manager message that is echoed is the policy's own text: it is built from rule names and detector reasons, never from values found in the input, and for output inspections (manager >= 0.1.16) it does not quote the tool output either. The detected values themselves travel only in the synchronous loopback guard response — they are never written to the span, the retry queue, or a log. The original masked/truncated input and output remain on the same telemetry span and spanId, together with the verdict and pinta.guard.target = "tool_output". The tool already ran: masking or withholding a result, or stopping a run, does not undo its effects. For the Local stop path, the result may remain in the transcript.

ALLOW, REVIEW, inactive guarding and existing fail-open behavior are unchanged. Internal control tools report_intent, ask_user and its documented PascalCase alias AskUserQuestion remain telemetry-only. Arbitrary case variants or similarly named MCP tools are not exempt.

Native failed-result enforcement is unsupported. PostToolUseFailure remains telemetry-only, not a protected phase. In native Copilot CLI 1.0.88, the failure hook fires for a failed view, but both continue: false and modifiedResult are ignored and the original error reaches the next model request. The hook's error may also be less complete than the model-facing result. A future supported host failure-result/batch contract is required; printing a denial alone would falsely claim protection.

Ordinary shell exit status 1 is a different case: Copilot delivers that result through successful PostToolUse, so it is eligible for the existing successful-result gate. Native hook phase, not a guessed exit-code pattern, determines which contract applies.

Enforcement requires synchronous hooks, a host honoring the documented Copilot result-replacement or VS Code Local stop contract, and completion within the host timeout. Added context, a PreToolUse-shaped denial, or exit code 2 alone is not output withholding. Deploy the corresponding guard-runtime projection update to Manager and backend before rolling out these adapters: older readers can miss native camelCase result fields or confuse output denial with a prevented tool call.

Span conventions

| Attribute | Value | |---|---| | ingest.type | "copilot" (aware-backend discriminator) | | copilot.hook | Hook event name (resolved from hook_event_name / hookEventName / hookName) | | copilot.surface | cli | ext | cloud (runtime-detected) | | copilot.model | Exact host-reported model ID, when attributable; never a placeholder | | copilot.model_source | Evidence for the model value (see below) | | copilot.<key> | Every other top-level field (Bronze flattening, raw key preserved) | | pinta.guard.target | tool_output for a non-null successful after-tool verdict; absent on before-tool gates | | service.name | "copilot" · telemetry.sdk.name "pinta-copilot" |

CLI ↔ ext payload differences (absorbed by the adapter)

| | CLI | ext | |---|---|---| | discriminator | hook_event_name (snake); permissionRequest uses hookName (camel) | hook_event_name (snake) | | tool result | tool_result (structured) | tool_response (Claude-style) | | tool_use_id | absent | present | | transcript_path | Stop only | every event | | subagent id | agent_name/agent_display_name | agent_id/agent_type | | permissionRequest | fires | not fired |

Bronze flattening preserves both shapes, except for the model normalization described below; the backend's CopilotIngestData normalizes (tool_response ?? tool_result, agent_id ?? agent_name, …).

Model attribution and its limits

copilot.model is a scalar ID, not JSON. An explicit hook model wins: a string or a descriptor's id (or name when no id field exists). Blank, unknown, undefined, null, n/a, none, auto and default values are omitted, case-insensitively. Control characters and IDs longer than 512 characters are rejected. This does not change the input event or bypass redaction. No provider/model is inferred from an agent name, CLI version, global model setting, process, or prompt.

JSON-stringified objects/arrays (including [object Object]) are not model IDs. For an explicit model, the host's model_source is retained when supplied; otherwise hook.model is used. Provider and requested/response fields remain unchanged. When transcript evidence replaces an unusable model, its source becomes model_source; any previous host source is kept as copilot.model_original_source rather than mislabeled as the new model's source.

| copilot.model_source | Evidence and scope | |---|---| | hook.model | The hook's explicit model. A SessionStart selection is not proof of the routed response model. | | transcript.assistant.message | assistant.message.data.model on a response requesting the hook's exact tool-call ID. | | transcript.tool.execution_start / transcript.tool.execution_complete | data.model on the exact tool execution. Matching assistant/execution records must agree; conflicts are omitted. | | transcript.session.selected | Startup selection from session.start.data.selectedModel plus model changes at/before the hook timestamp, only when the entire bounded history is available. Selected/requested, not routed. Never carried onto tools or resume events. |

The CLI lookup uses the supplied transcript_path/transcriptPath, or $COPILOT_HOME/session-state/<session_id>/events.jsonl (default home: ~/.copilot). It verifies the session header, timestamp, tool-call ID (tool_use_id, toolUseId, tool_call_id or toolCallId), an optional turn ID, and the exact subagent identity. Parent and child models cannot be inherited from one another. A future or merely latest response is never a fallback.

Many current CLI tool hooks omit a stable tool-call ID. Their model remains absent, even if a session selection or a recent response is visible. A subagent ID without its own transcript, an unrecognized IDE/cloud transcript format, or a record outside the read window also leaves the model absent. IDE/cloud hooks can always supply model directly. This is intentionally not a claim of complete model coverage for every host version.

Each lookup is read-only: at most a 256 KiB header plus a 1 MiB tail and 4,096 complete JSONL records, with no subprocess, directory scan, or cross-hook model cache. Explicit models and tool hooks without correlation IDs do no model lookup IO. Missing/unreadable files, non-regular files, leaf symlinks, malformed records and ambiguous metadata fail quietly; incomplete boundary lines are ignored. Transcript contents are never logged or forwarded by this lookup. Guard behavior, event counts, and the existing privacy pipeline are unchanged.

Architecture

src/
├── index.ts              # stdin → classify → trace → guard → span → exit 0 (always)
├── env-file.ts           # ~/.copilot/pinta-copilot.env loader (unset-only)
├── core/
│   ├── types.ts          # 3-way discriminator + snake/camel field absorption + classify
│   ├── surface.ts        # cli | cloud | ext detection (ELECTRON_RUN_AS_NODE, …; NOT TERM_PROGRAM)
│   ├── otlp.ts           # Bronze flattening (copilot.*) + ingest.type + surface + guard attrs
│   ├── trace.ts          # per-turn ULID trace, keyed by session_id
│   ├── transport.ts      # POST OTLP/HTTP traces (reads OTel env at call time)
│   ├── retry-queue.ts    # file-backed JSONL queue, flushed next invocation
│   ├── guard.ts          # POST PINTA_GUARD_ENDPOINT (100ms), fail-open
│   ├── redact.ts         # Tier-1 redaction + Tier-3 truncation
│   ├── config.ts / env-bridge.ts
└── tools/install-hooks.ts  # write/remove ~/.copilot/hooks/pinta-copilot.json

Development

npm install
npm run build         # tsc → dist/
npm test              # vitest
npm run smoke:model   # built CJS/ESM hooks -> isolated loopback collector
npm run mock-server   # local OTLP collector

The integration suite exercises both built CJS/ESM entrypoints against a loopback guard and collector: native/legacy result casing, CLI/cloud/Local contracts, retained original evidence, nonblocking verdicts and transport failures. Hook unit tests also pin denial-before-telemetry ordering.

smoke:model runs fresh hook processes with isolated HOME/plugin data and no manager/guard calls. It covers supplied, missing, placeholder, correlated, cross-session/subagent, future, partial and oversized transcript cases. Local results are written to .validation/model-smoke.json. An optional -- --baseline=<previous-built-index.js> compares paired end-to-end hook times (including Node startup and loopback HTTP, so scheduler noise is included).

License

PolyForm Noncommercial 1.0.0 — see LICENSE. Commercial use is not permitted; contact Pinta AI for a commercial license.