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.

Architecture
Three edges. One console. Policy flows back.
CI · laptop · pre-commit
$ bezoar scan
- static detectors, 4 bands
- 0 network requests
- text · json · sarif
bezoar.scan/v1↓bezoar ingest → POST /ingest
desktop daemon · macOS / Linux
$ bezoar guard
- 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
- tools/list scanned + pinned
- tools/call decided
- rug-pull on digest move
mcp_call decisions↓guard.sock or gateway.db
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.
- Inventoryassets · hosts · timelines
- Findingsfingerprint · state · owner · expiry
- Ingestsigned envelope · idempotent
- Notificationsslack · teams · email · webhook
- Policyhistory · diff · restore
- Frameworksowasp · atlas · att&ck · cwe
- Agent surfacellms.txt · .well-known
- Enterprise-as-YAMLoidc · rbac · audit · retention
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 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 eventsversion: 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: deny02mcp-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 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_pullversion: 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 it03fetch / 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 ./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 drift04inventoryconsole
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
$ 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
$ 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-opens06ingest / 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.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-slacknotifications:
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
$ 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 --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 family09agent 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 -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 onlyScan
Install, scan, gate.
Offline-first. Deterministic. CI-ready. Same inputs, same report.
Install
01One tool. No account. Python ≥ 3.11.
step 01$ uv tool install bezoar $ bezoar version bezoar 1.0.0a4Scan
02Point at a skill, MCP server, plugin, or repo. Four bands. Offline.
step 02$ bezoar scan ./my-skill --format sarif {"version":"2.1.0","runs":[…]}Gate
03Band 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.
| Component | Runs | Talks to | Stores |
|---|---|---|---|
| $ bezoar scan | CI · laptop · pre-commit | Nothing. | stdout only. |
| $ bezoar guard | macOS / Linux · user session | AF_UNIX until enrolled. | guard.db locally. |
| $ bezoar mcp-gateway | stdio wrap or 127.0.0.1:8090 | Your upstream MCP. | guard.sock or gateway.db. |
| $ bezoar serve | One container · one volume | Outbound 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.
uv tool install bezoar