pi-opa-net

OPA-backed bash command guard for the pi ecosystem — structured decision-output.v1 JSON, fail-open default, Claude Code hook protocol compatible. Agent-agnostic engine + CLI.

Packages

Package details

skill

Install pi-opa-net from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-opa-net
Package
pi-opa-net
Version
0.2.0
Published
Jul 20, 2026
Downloads
189/mo · 21/wk
Author
buihongduc132
License
MIT
Types
skill
Size
117.5 KB
Dependencies
3 dependencies · 0 peers
Pi manifest JSON
{
  "skills": [
    "./skills"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-opa-net

npm version CI License: MIT

OPA-backed bash command guard for the Pi ecosystem. Structured --json output (decision-output.v1 schema), fail-open default, exit-code compatible with the Claude Code hook protocol.

An agent-agnostic engine + CLI that evaluates shell commands against an OPA/Rego policy and emits a strict, auditable decision record. Designed as the decision backend for pi extensions, Claude Code hooks, scripts, and any agent that needs a uniform bash-guard contract.

Why

Three limitations of today's asymmetric, agent-specific guard output that this fixes:

# Limitation Fix
1 Asymmetric — allow is silent, deny emits a string Both allow AND deny emit the full schema
2 No provenance — only a human message reasons[].rule_id traces decision → rule → source line
3 Agent-specific — tied to one hook protocol Agent-agnostic wrapper; adapters become thin views

Status

  • Stable: v0.2.0 — schema v1.0 (additive), 42-rule catalog, capability-based unlock-keys, full TDD coverage (304 tests)
    • Cupcake-compatible policy: .cupcake/policies/claude/cc_safety_net_parity.rego (all 42 active cc-safety-net rules in OPA/Rego v1)
    • Capability-based unlock-keys (v0.2.0) — trusted agents present a per-rule salted HMAC key; TS-side post-eval filter demotes matching deny reasons.
  • Engine: OPA 1.x (lazy-loaded on every dev box)
  • Scope: bash command guarding only (see docs/locked-decisions.yaml LD3)
  • Pi extension: the thin tool_call adapter lives in a separate future repo (pi-opa-net-ext, per OT5) — this package is the engine + library

Features

  • OPA-backed decisions — every command evaluated by an OPA/Rego policy; 42-rule catalog (full cc-safety-net user-rule parity).
  • Symmetric structured output — both allow AND deny emit the full decision-output.v1 schema with reasons[].rule_id provenance, fail-mode observability, and parse-confidence surfacing.
  • Fail-open by default — never bricks the shell; matches the pi-safety-net fork guarantee. PI_OPA_FAIL_MODE=closed for fail-closed.
  • Capability-based unlock-keys (v0.2.0) — grant trusted agents a per-rule salted HMAC key (long-lived ll_<16hex> or TTL ttl.<exp>.<16hex>). TS-side post-eval filter demotes matching deny reasons. All-or-nothing multi-rule semantics; every bypass is auditable via source:'opa-unlocked'.
  • Claude Code hook compatible — exit codes 0 = allow, 2 = deny; JSON on stdout.
  • Pluggable seamsSaltResolver (deploy-local salt now, remote/keychain later) and AuditSink (decision-record only now, file/webhook later).

Installation

Prerequisites

OPA 1.x on PATH (recommended via mise):

mise install opa@latest
mise use -g opa@latest

Install

# as a library (pi extension / script consumer)
npm install pi-opa-net
# or
bun add pi-opa-net

# run the CLI directly via bun
bunx pi-opa-net eval "git stash pop"

For AI Agents (pi / OpenCode / Claude Code / Codex)

Add to your settings.json:

{
  "packages": ["pi-opa-net"]
}

Or tell your agent:

Install and configure pi-opa-net by following:
https://raw.githubusercontent.com/buihongduc132/pi-opa-net/refs/heads/main/README.md

For pi (git-sourced)

In settings.json:

{
  "packages": ["https://github.com/buihongduc132/pi-opa-net"]
}

Usage

CLI

# claude-code mode (default): suppress stdout on allow, JSON on deny
pi-opa-net eval "git stash pop"             # exit 2 + JSON on stdout
pi-opa-net eval "git stash list"            # exit 0, empty stdout

# --json: always emit the full decision-output.v1 schema
pi-opa-net eval "git stash pop" --json

# stdin
echo "docker stop foo" | pi-opa-net eval

Exit codes: 0 = allow, 2 = deny (Claude Code hook protocol compatible).

Programmatic API

import { configFromEnv, CommandParserCoordinator, OpaCliEngine, DecisionBuilder, OutputFormatter, RULES, RuleRegistry } from 'pi-opa-net';

const config = configFromEnv('/path/to/safety.rego');
const parser = new CommandParserCoordinator();
const engine = new OpaCliEngine(config);
const builder = new DecisionBuilder({
  config,
  registry: new RuleRegistry(RULES),
  digest: engine.rulebookDigest(),
});

const parsed = parser.parse('git stash pop');
const engineDecision = await engine.evaluate(parsed);
const output = builder.build(parsed, engineDecision);

console.log(output.decision);  // 'deny'
console.log(output.reasons[0].rule_id);  // 'block-git-stash-mutations'

Output schema

See schemas/decision-output.v1.json — JSON Schema draft 2020-12, strict (additionalProperties: false throughout). Every emitted record is validated against it before leaving the process.

{
  "schema_version": "1.0",
  "decision": "deny",            // allow | deny
  "action": "block",             // allow | block | prompt_user(v2) | log_only(v2)
  "source": "opa",               // opa | fail-open | fail-closed | cached
  "reasons": [                   // every fired deny rule → one entry
    { "rule_id": "block-git-stash-mutations",
      "message": "Do not mutate stashes in shared work...",
      "family": "git", "severity": "block" }
  ],
  "input": { "raw": "git stash pop", "program": "git",
             "subcommand": "stash", "args": ["pop"],
             "parse_confidence": "full" },   // full | partial | regex-only | failed
  "summary": "BLOCKED: git stash pop (rule: block-git-stash-mutations)",
  "suggestions": ["git stash list", "git stash show"],
  "metadata": { "engine": "opa", "opa_version": "1.18.1",
                "rulebook_digest": "dee3746bf7b5", "policy_path": "...",
                "hostname": "box", "session_id": "" },
  "evaluated_at": "2026-07-01T14:23:45.123Z",
  "decision_id": "7f3a9c2e-1b4d-4e8f-9a2c-5d6e7f8a9b01",
  "duration_ms": 4.2
}

Architecture

Two halves (per the design findings):

Half Responsibility Module
Parse raw "git stash list"{program, subcommand, args, parse_confidence} src/parser/
Decide structured input → allow/deny + reasons policy/safety.rego + src/engine/
src/
├── parser/     CommandParserCoordinator (hybrid: ShellQuote AST primary, regex fallback)
├── engine/     OpaCliEngine (subprocess `opa eval` + fail-mode)
├── rules/      RuleRegistry + catalog (message → rule_id + family provenance)
├── output/     DecisionBuilder (schema assembly) + OutputFormatter (stdout/exit-code)
├── config/     EngineConfig (fail-mode, OPA binary discovery)
├── cli/        run.ts (wires the pipeline)
└── util/       sha256Prefix (rulebook drift detection)

Design principles

  • OOPDecisionEngine and CommandParser interfaces; fakes injectable for tests.
  • DRYRuleRegistry is the single source of truth for rule provenance; the catalog mirrors policy/safety.rego message-for-message (a parity test fails on drift).
  • Observable — the source field makes fail-mode (open/closed) auditable per-decision; parse_confidence surfaces parser fidelity.

Fail-mode

When OPA is unreachable (cold-start, binary missing, timeout):

Mode Behavior source field
open (default) allow the command through fail-open
closed block the command fail-closed
PI_OPA_FAIL_MODE=closed pi-opa-net eval "git stash pop"

The default open matches the pi-safety-net fork's "never brick the shell" guarantee.

Configuration (env)

Var Default Purpose
PI_OPA_BINARY auto (PATH → mise) OPA binary path
PI_OPA_FAIL_MODE open fail-mode
PI_OPA_TIMEOUT_MS 250 OPA eval timeout
PI_OPA_HOSTNAME os.hostname() metadata.hostname
PI_OPA_SESSION_ID "" metadata.session_id

Develop

bun install
bun test                 # all tests (106)
bun test --coverage      # coverage (line > 98%)
bun run typecheck        # tsc --noEmit
bun run lint             # biome
bun run smoke            # one-shot CLI check

E2E tests run the live CLI against real OPA + the real policy, covering ≥40% of the 42-rule catalog.

Cupcake-compatible policy

This repo also ships a Cupcake-format OPA/Rego policy at .cupcake/policies/claude/cc_safety_net_parity.rego that ports all 42 active cc-safety-net user rules. It can be evaluated directly by OPA or consumed as a Cupcake catalog overlay. See docs/cupcake-parity.md for details, including the standalone opa eval examples and the input/output contract.

Decisions & open threads

Project health

Related

  • pi-safety-net — the fail-open fork of cc-safety-net (Path A: non-pi agents). pi-opa-net is Path B (OPA-backed, structured output).
  • cc-safety-net — upstream Claude Code safety net.

MIT © buihongduc132

Repository

GitHub: buihongduc132/pi-opa-net