Skip to content
bezoar

Platform · 1.0.0a4

From install to runtime, covered.

Scanner decides if an add-on should exist. Guard decides if a tool call runs. Gateway decides if an MCP server answers. Console holds the fleet. Same package, same license, all opt-in.

Illustrated bezoar console: findings list with one blocked row

Architecture

Three edges. One console. Policy flows back.

CI · laptop · pre-commit

$ bezoar scan

detail
  • static detectors, 4 bands
  • 0 network requests
  • text · json · sarif

bezoar.scan/v1↓bezoar ingest → POST /ingest

desktop daemon · macOS / Linux

$ bezoar guard

detail
  • hooks → allow / ask / deny
  • net + process sampling
  • hash-chained guard.db

guard-event.v1↓POST /guard/events · ← GET /guard/policy

in front of every MCP server

$ bezoar mcp-gateway

detail
  • tools/list scanned + pinned
  • tools/call decided
  • rug-pull on digest move

mcp_call decisions↓guard.sock or gateway.db

bezoar serve — your host

The console

One container. Your volume.

Stores and correlates what the edges send. Never computes a security fact. SQLite, hash-chained audit, loopback by default.

ghcr.io/…/bezoar-serve:1.0.0a4 · pin the tag, never latest

01guardedge

Desktop guard

allow / ask / deny before a tool runs. macOS and Linux.

User-level daemon on an AF_UNIX socket. Harness hooks call a shim on PreToolUse; the daemon returns allow / ask / deny against guard.yml. Refuses root. p95 shim budget: 60 ms.

The rest of it

Network is sampled, not intercepted — lsof on macOS, /proc on Linux. No trust store, no MITM, no packet filter. reverse_dns off by default.

Events land in a hash-chained SQLite log. bezoar guard verify recomputes the chain. Daemon down? The shim follows on_daemon_unavailable — allow or deny, your call.

Prompts
Tool payloads only. No prompt tap.
TLS / SNI
Connection table. That's it.
Packet block
User-level. net_connect is audit, not firewall.
Phone home
AF_UNIX only until you enroll.
Windows
macOS and Linux. Refuses cleanly elsewhere.

Source of truth: docs/guard.md

bezoar guard
$ bezoar guard install --harness claude_code --service
$ bezoar guard tail -n 3
deny   shell    curl … | sh          deny-curl-pipe-sh
ask    install  npx some-skill       unscanned install
alert  net      203.0.113.7:443      net_connect
$ bezoar guard verify
ok 4812 events
~/.config/bezoar/guard.yml
version: 1
mode: enforce            # audit (default) | enforce
on_daemon_unavailable: deny   # fail-closed
scan_on_install: true
net:
  sample_interval_ms: 1000
  reverse_dns: false      # zero getaddrinfo
defaults:
  shell: allow
  install: ask
  net_connect: alert
rules:
  - id: deny-curl-pipe-sh
    when:
      action: shell
      command_matches: '(?i)\b(curl|wget)\b[^|]*\|\s*(ba|z)?sh\b'
    decision: deny

02mcp-gatewayedge

MCP gateway

Every MCP server gets scanned, pinned, and gated.

Sits between harness and MCP server — stdio wrap or HTTP reverse proxy.install --client claude_code rewrites .mcp.json with a backup; uninstall restores it.

The rest of it

First tools/list is scanned and digest-pinned. Digest moves later? mcp_rug_pull. Denied tools never reach the model. tools/call is a guard decision — deny returns -32001, upstream never sees it.

Events to guard.sock or local gateway.db. Credentials from file refs — never logged. Not an OAuth server.

Source of truth: docs/mcp-gateway.md

bezoar mcp-gateway
$ bezoar mcp-gateway wrap -- npx -y @scope/mcp-server
tools/list  scanned  0 poisoned  digest pinned
tools/call  exec     deny  -32001 deny-exec
tools/list  digest moved → mcp_rug_pull
gateway.yml
version: 1
listen: 127.0.0.1:8090
policy: ~/.config/bezoar/guard.yml   # when.action: mcp_call
upstreams:
  - name: docs
    url: https://mcp.example.com/sse
    bearer_file: /run/secrets/mcp-docs  # never logged
    deny_tools: [exec, write_file]

$ bezoar mcp-gateway serve --config gateway.yml
$ bezoar mcp-gateway install --client claude_code --project
.mcp.json rewritten · backup written · uninstall restores it

03fetch / trustedge

Install interception

Download. Scan. Quarantine. Promote or reject.

bezoar fetch takes a spec — path, file://, npm:, pypi:, marketplace:, https:// — materialises local / file:// trees into quarantine, scans, installs nothing.

The rest of it

Approve, promote, pin the digest. Guard checks the pin on the next npx / uvx / pip install / git clone. Unpinned install? ask.

Source of truth: docs/quarantine.md

bezoar fetch
$ bezoar fetch ./skills/demo-skill
q_7f3a  band=potential_risk  quarantined, not installed
$ bezoar fetch approve q_7f3a
$ bezoar promote q_7f3a \
    --to ~/.claude/skills/demo-skill
✓ tree_sha256 pinned → trust.jsonl
$ bezoar trust verify ~/.claude/skills/demo-skill
ok no drift

04inventoryconsole

Fleet inventory

Every skill, MCP, plugin, hook — one row per asset, every host.

Derived inventory keyed platform:kind:name per repo. Same MCP on twelve laptops? One row, twelve hosts.

The rest of it

Fed by report import, CI ingest, guard heartbeats, and bezoar serve --reindex. Hosts tab: OS, version, mode, last seen, recent denies.

Source of truth: docs/inventory-lifecycle.md

GET /api/v1/inventory
$ curl -s $URL/api/v1/inventory?kind=mcp \
    | jq '.items[0]'
{
  "asset_key": "claude_code:mcp:filesystem",
  "repo": "github.com/acme/api",
  "latest_band": "potential_risk",
  "open_findings": 1,
  "last_seen": "2026-09-06T07:12:44Z"
}

05findingsconsole

Finding lifecycle

open · in progress · accepted · dismissed · fixed. Band stays a fact.

Findings dedupe by fingerprint. Lifecycle never rewrites a band. Same finding across a hundred CI runs — one row, hundred events.

The rest of it

Owner, comments, timeline. Accepted with expiry? Notification as the window closes, re-opens when it lapses. Promote-to-policy writes into .bezoar.yml — console and CLI agree.

Source of truth: docs/inventory-lifecycle.md

POST /api/v1/findings/{key}/state
$ curl -X POST $URL/api/v1/findings/$KEY/state \
    -d '{"status":"accepted",
         "exception_expires_at":"2026-12-01"}'
key = sha256(asset_key|fingerprint)[:32]
expiry near → notification · lapsed → re-opens

06ingest / notifyconsole

CI ingest + notifications

Ship scan reports from CI. Alert on Slack, Teams, email, webhook.

POST /api/v1/ingest takes a signed bezoar.scan/v1 envelope with repo, ref, commit. Idempotent on report bytes. Off until ingest.enabled: true.

The rest of it

Notifications: Slack, Teams, SMTP, HTTPS webhook. Persisted retry queue — failed delivery is an audit record, never a failed request.

Source of truth: docs/ci-ingest.md

bezoar ingest
$ bezoar ingest bezoar.json --url $URL \
    --token $INGEST_TOKEN --repo github.com/acme/api \
    --commit $GITHUB_SHA
200  id=ing_2c91  idempotent on report bytes + source
→ inventory updated · findings opened
→ rule on:[ingest] → #sec-slack
bezoar-serve.yml
notifications:
  enabled: true
  channels:
    - name: sec-slack
      type: slack
      webhook_url_file: /run/secrets/slack_webhook
    - name: generic
      type: webhook
      url: https://hooks.example.com/bezoar
      secret_file: /run/secrets/webhook_hmac
  rules:
    - on: [ingest, finding.accepted, guard.host_stale]
      channels: [sec-slack]
      filters: { min_severity: high }

07policy historyconsole

Policy as code, versioned

Every YAML edit tracked. Diff, restore, external drift detected.

.bezoar.yml and guard.yml — schema-validated, git-friendly, versioned in the console. History lists every sha; diff compares two; restore is one call.

The rest of it

Edited the file on the volume directly? Detected as external drift. Invalid policy is 422, never a silent write.

Source of truth: docs/console.md#policy-history

GET /api/v1/policy/history
$ curl -s $URL/api/v1/policy/history \
    | jq -r '.[] | [.sha[:7],.actor,.message] | @tsv'
9c1e2ab  security@example.com  raise unsafe → block
4b77f01  external              external edit
0a3d9e5  security@example.com  add stripe pin
$ curl -X POST $URL/api/v1/policy/diff \
    -d '{"from":"0a3d9e5","to":"9c1e2ab"}'

08frameworksconsole

Framework mappings

149 rules. OWASP, ATLAS, ATT&CK, NIST, EU AI Act, CWE. Run coverage, don't slide-deck it.

Every rule in the four bundled packs (149 at 1.0.0a4) carries a mappings: block. Findings in JSON and SARIF include the mapped ids.

The rest of it

bezoar rules coverage --framework <name> prints covered K/T. Run the command — we won't hand-edit numbers into numbers into docs or this page.

Source of truth: docs/frameworks.md

bezoar rules coverage
$ bezoar rules coverage --framework owasp_agentic
owasp_agentic coverage
  ASI01  …  N
  ASI02  …  N
covered K/T   # run it; we don't hand-edit numbers
$ bezoar rules lint --require-mappings
ok # refuses a pack missing a required family

09agent surfaceconsole

Agent-readable console

llms.txt · OpenAPI · MCP card · local stdio server. Zero socket.connect.

Unauthenticated surface: /llms.txt, /.well-known/openapi.json, /.well-known/mcp.json. No tokens, no report contents, no guard events.

The rest of it

bezoar mcp-server is local stdio MCP — scan, policy, rules, audit, guard tail. Does not upload prompts or file contents.

Source of truth: docs/agents.md

curl $URL/.well-known/mcp.json
$ curl -s $URL/.well-known/mcp.json | jq '.tools[].name'
"scan_path" "report_summary" "policy_validate"
"policy_explain" "rules_list" "rule_get"
"audit_verify" "guard_tail"
$ curl -s $URL/llms.txt | head -2
# bezoar — scanner for the AI-agent supply chain
$ bezoar mcp-server   # stdio · local only

Scan

Install, scan, gate.

Offline-first. Deterministic. CI-ready. Same inputs, same report.

  1. Install

    01

    One tool. No account. Python ≥ 3.11.

    step 01
    $ uv tool install bezoar
    $ bezoar version
    bezoar 1.0.0a4
  2. Scan

    02

    Point at a skill, MCP server, plugin, or repo. Four bands. Offline.

    step 02
    $ bezoar scan ./my-skill --format sarif
    {"version":"2.1.0","runs":[…]}
  3. Gate

    03

    Band is fact. Decision is policy — .bezoar.yml, CI, baselines. Exit 1 means blocked.

    step 03
    $ bezoar scan . --fail-on unsafe \
        --baseline .bezoar-baseline.json
    ✓  band=safe  decision=allow

Boundaries

Where it runs. What it touches.

Runtime location, network contact and storage per component
ComponentRunsTalks toStores
$ bezoar scanCI · laptop · pre-commitNothing.stdout only.
$ bezoar guardmacOS / Linux · user sessionAF_UNIX until enrolled.guard.db locally.
$ bezoar mcp-gatewaystdio wrap or 127.0.0.1:8090Your upstream MCP.guard.sock or gateway.db.
$ bezoar serveOne container · one volumeOutbound if notifications on.state.db · reports · audit.

No.

Not in 1.0.0a4.

We'd rather say it here than behind a demo call.

Windows guard
macOS and Linux. Scanner and console run everywhere.
MCP OAuth
We proxy. We don't issue tokens.
DNS / SNI / packet block
Connection table only. No MITM. No firewall.
Remote fetch
Specs recorded. a4 materialises local and file:// only.
On by default
ingest, notifications, guard — all off until you flip them.

One install. Everything's in it.

Scan today. Guard when you're ready. Compose up for the fleet. Nothing to unlock.