protect-mcp 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2: run the gate in other agents (Codex, Cursor, Gemini, Hermes)
4
+
5
+ The `evaluate` and `sign` verbs now accept `--format <host>`
6
+ (claude | codex | gemini | cursor | hermes | grok). With it, the verb reads the
7
+ host's hook payload from stdin (tool name and input) and emits the deny verdict
8
+ in that host's hook contract, so the same fail-closed Cedar gate works as a
9
+ PreToolUse/PostToolUse hook outside Claude Code.
10
+
11
+ The load-bearing detail: **Hermes ignores hook exit codes** and reads the verdict
12
+ from stdout, so `--format hermes` denies via `{"decision":"block"}` on stdout. A
13
+ raw exit-2 (which every other host honors) would have silently failed open there.
14
+ Cursor and Gemini receive their structured stdout deny verdict in addition to
15
+ exit 2. Without `--format`, the verbs behave exactly as before (the
16
+ `--tool`/`--input` flag mode is unchanged).
17
+
18
+ ## 0.7.1: documentation and security policy
19
+
20
+ No code change from 0.7.0. Rewrote the README to lead with the fail-closed and
21
+ self-test guarantees, and added a `SECURITY.md` disclosure policy (supported
22
+ versions, the affected 0.5.x/0.6.x range, the published advisory, and the
23
+ coordinated-disclosure process). The package now ships `SECURITY.md`.
24
+
3
25
  ## 0.7.0 (security release): the gate now fails closed and actually evaluates
4
26
 
5
27
  This release fixes the way the Cedar policy gate behaves when anything goes
package/README.md CHANGED
@@ -1,240 +1,142 @@
1
1
  # protect-mcp
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/protect-mcp)](https://www.npmjs.com/package/protect-mcp)
4
- [![Downloads](https://img.shields.io/npm/dm/protect-mcp)](https://www.npmjs.com/package/protect-mcp)
5
- [![Claude Code Marketplace](https://img.shields.io/badge/Claude%20Code-marketplace-d97757?logo=anthropic&logoColor=white)](https://github.com/wshobson/agents/tree/main/plugins/protect-mcp)
6
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
-
8
- > A policy check that sits between your AI agent and the tools it calls.
9
- > Every tool call is evaluated against a rule you wrote. Every decision is signed.
10
-
11
- > **Receipt format:** `protect-mcp` emits Veritas Acta receipts. Legacy
12
- > ScopeBlind receipts remain verifiable, but Acta v0.1 is the canonical
13
- > format going forward. Spec: [`@veritasacta/protocol`](https://www.npmjs.com/package/@veritasacta/protocol)
14
- > · IETF: [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/).
15
-
16
- ## What it does, in plain English
17
-
18
- When an AI agent (Claude Code, Cursor, a custom LangChain app, anything that
19
- uses the Model Context Protocol) wants to run a command, edit a file, or call
20
- an API, `protect-mcp` intercepts that request before it executes:
21
-
22
- 1. **Checks a policy.** You write rules in [Cedar](https://www.cedarpolicy.com/)
23
- — the same policy language AWS uses for IAM. Rules like *"never allow
24
- `rm -rf /`"*, *"only allow `Bash` during working hours"*, or *"require
25
- human approval for anything touching production."*
26
- 2. **Returns a decision.** `allow`, `deny`, or `request_approval`. The agent
27
- framework respects the decision — if it's `deny`, the tool call doesn't run.
28
- 3. **Signs a receipt.** An Ed25519-signed, hash-chained record of the decision,
29
- written to `.receipts/`. Verifiable offline by anyone with the public key.
30
- When your auditor asks *"what did that agent do on 2026-03-14?"* you have
31
- cryptographic proof — not a log file you might have tampered with.
32
-
33
- You install it once. Your agent keeps working the same way. The difference:
34
- every action it takes is now policy-checked and audit-evidenced.
35
-
36
- ## Who this is for
37
-
38
- - **Developers using Claude Code / Cursor / Cline** who want *"don't let the
39
- agent delete my repo"* enforced rather than hoped for.
40
- - **Security teams** shipping agents to engineering who need a portable
41
- policy layer that travels across frameworks.
42
- - **Compliance teams** who need tamper-evident evidence of what agents did,
43
- verifiable without trusting the vendor.
44
-
45
- ## How it relates to `sb-runtime`
46
-
47
- `protect-mcp` is a **library** — it sits inside your agent framework and
48
- gates tool calls cooperatively. Right tool when you own the agent's code and
49
- trust its framework to honour decisions.
50
-
51
- [`sb-runtime`](https://github.com/ScopeBlind/sb-runtime) is the companion
52
- **binary** that wraps the whole agent *process* in an OS-level sandbox
53
- (Landlock + seccomp on Linux). It enforces decisions at the kernel layer —
54
- the agent can't ignore them even if it tried. Use `sb-runtime` when you're
55
- running an agent you didn't write, or when you want belt-and-braces defence
56
- in depth:
57
-
58
- ```
59
- ┌─────────────────────────────────────────────────┐
60
- │ sb-runtime ← OS refuses forbidden syscalls │
61
- │ ┌───────────────────────────────────────────┐ │
62
- │ │ agent process (Claude Code, Python, …) │ │
63
- │ │ ┌─────────────────────────────────────┐ │ │
64
- │ │ │ protect-mcp │ │ │
65
- │ │ │ ← Cedar decides per tool call, │ │ │
66
- │ │ │ receipts every decision │ │ │
67
- │ │ └─────────────────────────────────────┘ │ │
68
- │ └───────────────────────────────────────────┘ │
69
- └─────────────────────────────────────────────────┘
70
- ```
3
+ Fail-closed Cedar policy gate plus signed receipts for AI agent tool calls.
71
4
 
72
- **One-liner:** `protect-mcp` is the policy hook inside your agent.
73
- `sb-runtime` is the OS sandbox around it. You want both.
74
-
75
- ---
76
-
77
- ## Quick Start Claude Code
78
-
79
- Two commands. Every tool call is receipted.
5
+ [![npm version](https://img.shields.io/npm/v/protect-mcp)](https://www.npmjs.com/package/protect-mcp)
6
+ [![downloads](https://img.shields.io/npm/dm/protect-mcp)](https://www.npmjs.com/package/protect-mcp)
7
+ [![license](https://img.shields.io/npm/l/protect-mcp)](https://www.npmjs.com/package/protect-mcp)
8
+ [![node](https://img.shields.io/node/v/protect-mcp)](https://www.npmjs.com/package/protect-mcp)
9
+
10
+ `protect-mcp` is a gate that sits in front of an AI agent's tool calls. It evaluates
11
+ each call against a [Cedar](https://www.cedarpolicy.com/) policy (the same language
12
+ AWS uses for IAM), blocks what breaks the rules before it runs, and signs an
13
+ offline-verifiable Ed25519 receipt of every decision. It runs locally, sends no
14
+ telemetry of your decisions anywhere, and is MIT licensed.
15
+
16
+ ## Why it is different
17
+
18
+ - **Fail-closed by default.** On any policy error, a missing engine, or an
19
+ evaluation failure, the decision is DENY. The gate never silently allows. An
20
+ observe mode exists for shadow rollout, but even there a call that would be
21
+ blocked is flagged `would_deny: true`, so a failure is never silent.
22
+ - **It proves its own restraint.** `serve --enforce` and `doctor` run a startup
23
+ self-test and refuse to arm the gate unless they can show that a known-forbidden
24
+ action is actually denied. A gate that cannot prove it denies does not start.
25
+ - **Every decision is a receipt anyone can verify.** Decisions are Ed25519-signed
26
+ and verifiable offline with [`@veritasacta/verify`](https://www.npmjs.com/package/@veritasacta/verify).
27
+ No vendor trust required: the math does not care who runs it.
28
+
29
+ ## Quickstart (30 seconds)
80
30
 
81
31
  ```bash
82
- # 1. Generate hooks, keys, Cedar policy, and /verify-receipt skill
83
- npx protect-mcp init-hooks
32
+ # 1. Generate an Ed25519 keypair, a config template, and a sample policy.
33
+ npx protect-mcp init
84
34
 
85
- # 2. Start the hook server
86
- npx protect-mcp serve
35
+ # 2. Put a Cedar policy in ./cedar (see "Write a policy" below), then serve
36
+ # the Claude Code hook gate in enforce mode. It runs a restraint self-test
37
+ # first and refuses to start if it cannot prove it denies a forbidden vector.
38
+ npx protect-mcp serve --enforce --cedar ./cedar
87
39
  ```
88
40
 
89
- Open Claude Code in the same project. Every tool call is now intercepted, evaluated, and signed.
90
-
91
- To also connect to the ScopeBlind dashboard in one step:
41
+ One-shot evaluation, the way a PreToolUse hook calls it. Exit code 2 means deny
42
+ (the tool is blocked); exit 0 means allow:
92
43
 
93
44
  ```bash
94
- npx protect-mcp quickstart --connect
95
- ```
96
-
97
- Creates a free ScopeBlind dashboard and configures receipt upload automatically. No signup required. Free up to 20,000 receipts/month.
98
-
99
- ### What `init-hooks` creates
100
-
101
- | File | Purpose |
102
- |------|---------|
103
- | `.claude/settings.json` | Hook config (PreToolUse, PostToolUse, + 9 lifecycle events) |
104
- | `keys/gateway.json` | Ed25519 signing keypair (auto-gitignored) |
105
- | `policies/agent.cedar` | Starter Cedar policy — customize to your needs |
106
- | `protect-mcp.json` | JSON policy with signing + rate limits |
107
- | `.claude/skills/verify-receipt/SKILL.md` | `/verify-receipt` skill for Claude Code |
108
-
109
- ### Architecture
45
+ npx protect-mcp evaluate --cedar ./cedar --tool Bash --input '{"command":"rm"}'
46
+ echo $? # 2 -> denied, fail-closed
110
47
 
48
+ npx protect-mcp evaluate --cedar ./cedar --tool Read --input '{"path":"README.md"}'
49
+ echo $? # 0 -> allowed
111
50
  ```
112
- Claude Code → POST /hook → protect-mcp (Cedar + sign) → response
113
-
114
- .protect-mcp-log.jsonl
115
- .protect-mcp-receipts.jsonl
116
- ```
117
-
118
- - **PreToolUse**: synchronous Cedar policy check → deny blocks the tool
119
- - **PostToolUse**: async receipt signing → zero latency impact
120
- - **deny is architecturally final** — it cannot be overridden by the model or other hooks
121
-
122
- ### Endpoints
123
-
124
- | Method | Path | Description |
125
- |--------|------|-------------|
126
- | POST | `/hook` | Claude Code hook endpoint |
127
- | GET | `/health` | Server status, policy info, signer info |
128
- | GET | `/receipts` | Recent signed receipts |
129
- | GET | `/receipts/latest` | Most recent receipt |
130
- | GET | `/suggestions` | Auto-generated Cedar policy fix suggestions |
131
- | GET | `/alerts` | Config tamper detection alerts |
132
51
 
133
- ### Verify receipts
52
+ A missing or unloadable policy denies (exit 2) unless you explicitly pass
53
+ `--fail-on-missing-policy false`.
134
54
 
135
- ```bash
136
- # Inside Claude Code:
137
- /verify-receipt
55
+ ## Claude Code hooks
138
56
 
139
- # From terminal:
140
- curl http://127.0.0.1:9377/receipts/latest | jq .
141
- npx protect-mcp receipts
57
+ `protect-mcp init-hooks` writes a `.claude/settings.json` for you. To wire the
58
+ gate by hand, the two verbs you need are `evaluate` (PreToolUse, blocks on exit 2)
59
+ and `sign` (PostToolUse, records a receipt). Pin the version so a Claude Code
60
+ session always runs the gate you tested:
142
61
 
143
- # Check policy suggestions:
144
- curl http://127.0.0.1:9377/suggestions | jq .
62
+ ```json
63
+ {
64
+ "hooks": {
65
+ "PreToolUse": [
66
+ {
67
+ "matcher": "",
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "npx protect-mcp@0.7.0 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
72
+ }
73
+ ]
74
+ }
75
+ ],
76
+ "PostToolUse": [
77
+ {
78
+ "matcher": "",
79
+ "hooks": [
80
+ {
81
+ "type": "command",
82
+ "command": "npx protect-mcp@0.7.0 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
83
+ }
84
+ ]
85
+ }
86
+ ]
87
+ }
88
+ }
145
89
  ```
146
90
 
147
- ## Quick Start MCP Server Wrapper
148
-
149
- Wrap any stdio MCP server as a transparent proxy:
91
+ `evaluate` exits 2 on deny so Claude Code blocks the tool call, and 0 on allow.
92
+ `sign` is best-effort: it appends an Ed25519-signed receipt when a key is
93
+ configured, and if no signer is available it records an honest unsigned line
94
+ (`"signed": false`) rather than failing the tool.
150
95
 
151
- ```bash
152
- # Shadow mode — log every tool call, enforce nothing
153
- npx protect-mcp -- node my-server.js
96
+ ## Use it in other agents (Codex, Cursor, Gemini, Hermes)
154
97
 
155
- # Enforce mode with policy
156
- npx protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
98
+ The same fail-closed gate runs as a tool hook in any agent that supports them. Add
99
+ `--format <host>` so the verb reads that host's hook payload from stdin and denies
100
+ in its contract:
157
101
 
158
- # Generate keys + config template
159
- npx protect-mcp init
102
+ ```bash
103
+ # the PreToolUse / before-tool command for each host
104
+ npx -y protect-mcp@latest evaluate --format codex --cedar ./cedar # OpenAI Codex
105
+ npx -y protect-mcp@latest evaluate --format gemini --cedar ./cedar # Gemini CLI BeforeTool
106
+ npx -y protect-mcp@latest evaluate --format cursor --cedar ./cedar # Cursor beforeShellExecution
107
+ npx -y protect-mcp@latest evaluate --format hermes --cedar ./cedar # Hermes pre_tool_call
160
108
  ```
161
109
 
162
- ## How It Works
163
-
164
- protect-mcp evaluates every tool call against a policy (JSON, Cedar, or external PDP), signs the decision as an Ed25519 receipt, and logs the result.
165
-
166
- **Two integration modes:**
110
+ Pair each with `sign --format <host>` on the post-tool event for receipts. The
111
+ important case is **Hermes**, which ignores hook exit codes and reads the verdict
112
+ from stdout, so `--format hermes` denies via `{"decision":"block"}` rather than
113
+ exit 2 (a raw exit-2 would silently fail open there). Without `--format`, the
114
+ verbs read `--tool`/`--input` flags exactly as in the Claude Code section above.
167
115
 
168
- | Mode | Transport | Use Case |
169
- |------|-----------|----------|
170
- | Hook Server | HTTP (`npx protect-mcp serve`) | Claude Code, agent swarms |
171
- | Stdio Proxy | stdin/stdout (`npx protect-mcp -- ...`) | Claude Desktop, Cursor, any MCP client |
116
+ ## Write a policy
172
117
 
173
- **Three policy engines:**
174
-
175
- | Engine | Config | Notes |
176
- |--------|--------|-------|
177
- | JSON | `--policy policy.json` | Simple per-tool rules |
178
- | Cedar | `--cedar ./policies/` | Local WASM evaluation via `@cedar-policy/cedar-wasm` |
179
- | External PDP | `policy_engine: "external"` | OPA, Cerbos, or any HTTP PDP |
180
-
181
- ## Swarm Tracking
182
-
183
- In multi-agent sessions, protect-mcp automatically tracks the swarm topology.
184
-
185
- **11 hook events handled:**
186
-
187
- | Event | Type | Description |
188
- |-------|------|-------------|
189
- | `PreToolUse` | Sync | Cedar/policy evaluation before tool execution |
190
- | `PostToolUse` | Async | Receipt signing after tool execution |
191
- | `SubagentStart` / `SubagentStop` | Lifecycle | Worker agent spawn/completion |
192
- | `TaskCreated` / `TaskCompleted` | Lifecycle | Coordinator task assignment |
193
- | `SessionStart` / `SessionEnd` | Lifecycle | Session lifecycle with sandbox detection |
194
- | `TeammateIdle` | Lifecycle | Agent utilization monitoring |
195
- | `ConfigChange` | Security | Tamper detection for `.claude/settings.json` |
196
- | `Stop` | Lifecycle | Finalization + policy suggestion summary |
197
-
198
- Each receipt includes:
199
- - `swarm.agent_id`, `swarm.agent_type`, `swarm.team_name`
200
- - `timing.tool_duration_ms`, `timing.hook_latency_ms`
201
- - `payload_digest` (SHA-256 hash for payloads >1KB)
202
- - `deny_iteration` (retry count after denial)
203
- - `sandbox_state` (enabled/disabled/unavailable)
204
- - OpenTelemetry `otel_trace_id` and `otel_span_id`
205
-
206
- ## Policy File
207
-
208
- ```json
209
- {
210
- "default_tier": "unknown",
211
- "tools": {
212
- "dangerous_tool": { "block": true },
213
- "admin_tool": { "min_tier": "signed-known", "rate_limit": "5/hour" },
214
- "read_tool": { "require": "any", "rate_limit": "100/hour" },
215
- "*": { "rate_limit": "500/hour" }
216
- },
217
- "signing": {
218
- "key_path": "./keys/gateway.json",
219
- "issuer": "protect-mcp",
220
- "enabled": true
221
- }
222
- }
223
- ```
224
-
225
- ### Cedar Policies
226
-
227
- Cedar deny decisions are **authoritative** — they cannot be overridden.
118
+ Cedar policies live in a directory you point at with `--cedar`. A `forbid` rule
119
+ denies, a `permit` rule allows. To match against a value in the tool input, use
120
+ the `.contains()` idiom:
228
121
 
229
122
  ```cedar
230
- // Allow read-only tools
123
+ // Allow read-only tools.
231
124
  permit(
232
125
  principal,
233
126
  action == Action::"MCP::Tool::call",
234
127
  resource == Tool::"Read"
235
128
  );
236
129
 
237
- // Block destructive tools
130
+ // Deny dangerous shell commands by matching the command against a list.
131
+ forbid(
132
+ principal,
133
+ action == Action::"MCP::Tool::call",
134
+ resource == Tool::"Bash"
135
+ ) when {
136
+ ["rm", "dd", "mkfs"].contains(context.command)
137
+ };
138
+
139
+ // Block destructive tools outright.
238
140
  forbid(
239
141
  principal,
240
142
  action == Action::"MCP::Tool::call",
@@ -242,239 +144,67 @@ forbid(
242
144
  );
243
145
  ```
244
146
 
245
- When a tool is denied, protect-mcp auto-suggests the minimal Cedar `permit()` rule via `GET /suggestions`.
246
-
247
- ## CVE-Anchored Policy Packs
248
-
249
- Each prevents a real attack:
250
-
251
- | Policy | Incident | OWASP |
252
- |--------|----------|-------|
253
- | `clinejection.json` | CVE-2025-6514: MCP OAuth proxy hijack (437K environments) | A01, A03 |
254
- | `terraform-destroy.json` | Autonomous Terraform agent destroys production | A05, A06 |
255
- | `github-mcp-hijack.json` | Prompt injection via crafted GitHub issue | A01, A02, A03 |
256
- | `data-exfiltration.json` | Agent data theft via outbound tool abuse | A02, A04 |
257
- | `financial-safe.json` | Unauthorized financial transaction | A05, A06 |
258
-
259
- Cedar equivalents available in `policies/cedar/`.
260
-
261
- ## MCP Client Configuration
262
-
263
- ### Claude Desktop
264
-
265
- ```json
266
- {
267
- "mcpServers": {
268
- "my-protected-server": {
269
- "command": "npx",
270
- "args": [
271
- "-y", "protect-mcp",
272
- "--policy", "/path/to/protect-mcp.json",
273
- "--enforce",
274
- "--", "node", "my-server.js"
275
- ]
276
- }
277
- }
278
- }
279
- ```
280
-
281
- ### Cursor / VS Code
282
-
283
- Same pattern — replace the server command with `protect-mcp` wrapping it.
284
-
285
- ## CLI Commands
286
-
287
- ```
288
- Commands:
289
- serve Start HTTP hook server for Claude Code (port 9377)
290
- init-hooks Generate Claude Code hook config + skill + sample Cedar policy
291
- quickstart Zero-config onboarding: init + demo + show receipts
292
- connect Link to ScopeBlind dashboard (creates sandbox if needed)
293
- init Generate Ed25519 keypair + config template
294
- demo Start a demo server wrapped with protect-mcp
295
- doctor Check your setup: keys, policies, verifier, connectivity
296
- trace <id> Visualize the receipt DAG from a given receipt_id
297
- status Show tool call statistics from the decision log
298
- digest Generate a human-readable summary of agent activity
299
- receipts Show recent persisted signed receipts
300
- bundle Export an offline-verifiable audit bundle
301
- simulate Dry-run a policy against recorded tool calls
302
- report Generate a compliance report from an audit bundle
303
-
304
- Options:
305
- --policy <path> Policy/config JSON file
306
- --cedar <dir> Cedar policy directory
307
- --enforce Enable enforcement mode (default: shadow)
308
- --port <port> HTTP server port (default: 9377 for serve)
309
- --verbose Enable debug logging
310
- ```
311
-
312
- ## Decision Logs
313
-
314
- Every tool call emits structured JSON to `stderr`:
315
-
316
- ```json
317
- [PROTECT_MCP] {"v":2,"tool":"read_file","decision":"allow","reason_code":"cedar_allow","policy_digest":"a1b2c3...","mode":"enforce","hook_event":"PreToolUse","timing":{"hook_latency_ms":1},"otel_trace_id":"..."}
318
- ```
319
-
320
- When signing is configured, a signed receipt is persisted to `.protect-mcp-receipts.jsonl`.
321
-
322
- ## Audit Bundles
323
-
324
- ```bash
325
- npx protect-mcp bundle --output audit.json
326
- ```
327
-
328
- Self-contained offline-verifiable bundle with receipts + signing keys. Verify with `npx @veritasacta/verify`.
329
-
330
- ## Dashboard
331
-
332
- protect-mcp works fully offline. Optionally connect to the ScopeBlind dashboard for:
333
-
334
- - Real-time receipt visualization
335
- - Abuse alerts and anomaly detection
336
- - Compliance export and audit bundles
337
- - Usage analytics
338
-
339
- Free tier: 20,000 receipts/month. No credit card required.
147
+ > **Hazard:** do NOT write `context.command in ["rm", "dd"]` to match a string
148
+ > against a list. `in` is for entity hierarchies, not string membership. Cedar
149
+ > treats the expression as a type error and silently discards the whole `forbid`
150
+ > rule, which (under a fail-open gate) leaves a residual `permit` standing. This
151
+ > is the exact defect behind the advisory below. Use `[...].contains(context.command)`
152
+ > instead. From 0.7.0 the gate denies on that error rather than permitting, and a
153
+ > CI tripwire test fails the build if the pattern is reintroduced into a shipped
154
+ > policy. See [GHSA-hm46-7j72-rpv9](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/GHSA-hm46-7j72-rpv9).
340
155
 
341
- [scopeblind.com/pricing](https://scopeblind.com/pricing)
156
+ Ready-to-use Cedar packs ship in `policies/cedar/` (Clinejection / CVE-2025-6514,
157
+ Terraform destroy, secret-file exfiltration, spending authority).
342
158
 
343
- ### ScopeBlind tenant integration (Founding Plan)
159
+ ## Verify a receipt
344
160
 
345
- If you have a ScopeBlind Founding Plan tenant, set your `SCOPEBLIND_TOKEN`
346
- (from the welcome email) in the environment and protect-mcp forwards every
347
- signed receipt to your dashboard at `https://scopeblind.com/console/<your-slug>`.
161
+ Receipts are signed and verifiable offline by anyone with the public key. No
162
+ network, no vendor, no trust in ScopeBlind:
348
163
 
349
164
  ```bash
350
- # Your protect-mcp install with cloud-synced receipts
351
- SCOPEBLIND_TOKEN=scp_... \
352
- npx protect-mcp init-hooks
165
+ npx @veritasacta/verify ./receipts/receipts.jsonl --format jsonl
166
+ # Exit 0 = valid, non-zero = tampered or malformed
353
167
  ```
354
168
 
355
- How it works:
169
+ `npx protect-mcp bundle --output audit.json` exports a self-contained,
170
+ offline-verifiable audit bundle of your receipts plus the public signing key.
356
171
 
357
- 1. On first receipt, the bridge exchanges your token for a short-lived BRASS-v2
358
- auth proof at `/fn/brass/issue` (ECDSA P-256 signed, hourly expiry).
359
- 2. Receipts are batched and POSTed to `/fn/console/<slug>/receipts` every 5
360
- seconds (up to 128 per batch).
361
- 3. The local `.receipts/` chain remains authoritative regardless of forward
362
- status. Network failures are retried; quota exhaustion is reported. No
363
- crash, no blocking.
172
+ ## Security
364
173
 
365
- Quota: founding tier 10,000 receipts/day, enterprise 100,000/day. Anything
366
- above quota is rejected with a structured response; receipts stay safe in
367
- `.receipts/` until you upgrade or until tomorrow.
174
+ `protect-mcp` 0.7.0 fails closed by design. On any policy-evaluation error, a
175
+ missing engine, or a policy that errored at evaluation, the decision is DENY,
176
+ not allow. `serve --enforce` and `doctor` run a boot self-test that proves the
177
+ gate denies a known-forbidden vector before it is trusted, and refuse to arm if
178
+ it cannot.
368
179
 
369
- Receipt verification by third parties:
180
+ **Affected versions: 0.5.x and 0.6.x.** Those lines fail open (they return ALLOW
181
+ on evaluation error) and do not evaluate Cedar correctly against the pinned
182
+ engine, so a `forbid` rule could fail to block. **Upgrade to >= 0.7.0.**
370
183
 
371
- ```bash
372
- # Anyone can verify your receipts offline using the public JWKS
373
- curl https://scopeblind.com/.well-known/jwks.json
374
- npx @veritasacta/verify .receipts/0001.json
375
- ```
376
-
377
- Verification is independent of ScopeBlind — the math doesn't care who runs it.
378
-
379
- ## Interoperability
184
+ Details and remediation: [GHSA-hm46-7j72-rpv9](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/GHSA-hm46-7j72-rpv9).
185
+ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
380
186
 
381
- The receipt format is independently implemented and verified across multiple systems:
187
+ ## Commands
382
188
 
383
- | Evidence | Detail |
384
- |----------|--------|
385
- | **4 independent implementations** | TypeScript (protect-mcp), Python (protect-mcp-adk), Rust (Cedar WASM), APS ProxyGateway |
386
- | **2 IETF Internet-Drafts** | [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/), [draft-pidlisnyi-aps-00](https://datatracker.ietf.org/doc/draft-pidlisnyi-aps/) |
387
- | **8 cross-engine receipts** | [Composition test](https://github.com/ScopeBlind/examples/tree/main/interop/composition-test): 2 engines, 1 verifier, all VALID |
388
- | **PRs merged into microsoft/agent-governance-toolkit** | [#667](https://github.com/microsoft/agent-governance-toolkit/pull/667), [#1159](https://github.com/microsoft/agent-governance-toolkit/pull/1159), [#1168](https://github.com/microsoft/agent-governance-toolkit/pull/1168), [#1186](https://github.com/microsoft/agent-governance-toolkit/pull/1186), [#1197](https://github.com/microsoft/agent-governance-toolkit/pull/1197), [#1202](https://github.com/microsoft/agent-governance-toolkit/pull/1202), [#1203](https://github.com/microsoft/agent-governance-toolkit/pull/1203), [#1205](https://github.com/microsoft/agent-governance-toolkit/pull/1205) |
389
- | **1 verifier, zero dependencies** | `npx @veritasacta/verify receipt.json --key <hex>` (Apache-2.0, offline) |
189
+ | Command | Description |
190
+ |---------|-------------|
191
+ | `serve` | Start the HTTP hook server for Claude Code (port 9377). `--enforce` runs the restraint self-test first; `--cedar <dir>` and `--policy <path>` select the policy. |
192
+ | `init` | Generate an Ed25519 keypair (`keys/gateway.json`), a config template, and a sample policy. |
193
+ | `evaluate` | Evaluate one tool call against a Cedar policy (PreToolUse gate). Exit 2 = deny (fail-closed), exit 0 = allow. |
194
+ | `sign` | Sign one tool call into a receipt (PostToolUse). Best-effort: records an honest unsigned line if no key. |
195
+ | `simulate` | Dry-run a policy against a recorded decision log to see what it would have blocked. |
196
+ | `demo` | Start a built-in demo server wrapped with the gate, to see receipts instantly. |
197
+ | `doctor` | Check your setup (keys, policies, Cedar engine, verifier) and run the restraint self-test. |
198
+ | `bundle` | Export an offline-verifiable audit bundle of receipts plus the public key. |
199
+ | `report` | Generate a compliance report (Markdown or JSON) from the decision log and receipts. |
390
200
 
391
- Verify any receipt from any implementation:
392
-
393
- ```bash
394
- npx @veritasacta/verify receipt.json --key <public-key-hex>
395
- # Exit 0 = valid, 1 = tampered, 2 = malformed
396
- ```
397
-
398
- ## Standards & IP
399
-
400
- - **IETF Internet-Draft**: [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
401
- - **Patent Status**: 4 Australian provisional patents pending (2025-2026)
402
- - **Cedar WASM**: [PR #64](https://github.com/cedar-policy/cedar-for-agents/pull/64) merged + [PR #73](https://github.com/cedar-policy/cedar-for-agents/pull/73) (RequestGenerator, pending review)
403
-
404
- ## What's New in v0.7.0 (security release)
405
-
406
- The gate now fails closed. On any policy-evaluation error, a missing engine, or a
407
- policy that errored, the decision is DENY, not allow. Cedar is also evaluated
408
- correctly against cedar-wasm 4.x (earlier versions passed the policy in a shape the
409
- engine rejected, so evaluation errored and the old fail-open default allowed
410
- everything). `serve --enforce` now runs a restraint self-test before arming and
411
- refuses to start if it cannot prove it denies a forbidden vector, `doctor` reports
412
- that self-test, and one-shot `evaluate` and `sign` verbs are available for hooks.
413
- Upgrade from any 0.5.x or 0.6.x. See CHANGELOG.md.
414
-
415
- ## What's New in v0.5.3
416
-
417
- - `quickstart --connect`: Auto-create dashboard sandbox and configure receipt upload
418
- - `connect` subcommand: Link an existing setup to the ScopeBlind dashboard
419
- - Anonymous install telemetry (opt-out: `PROTECT_MCP_TELEMETRY=off`)
420
- - Improved Cedar WASM detection
421
-
422
- ## Cybersecurity: Vulnerability Disclosure Receipts
423
-
424
- protect-mcp provides the infrastructure for receipt-signed vulnerability disclosure workflows. When AI security agents (Claude Code Security, Mythos, or similar) discover vulnerabilities, every step of the disclosure lifecycle can produce a signed, chain-linked receipt:
425
-
426
- ```
427
- DISCOVER → DISCLOSE → PATCH → DEPLOY
428
- (Each step: Ed25519-signed, chain-linked, Cedar policy-bound)
429
- ```
430
-
431
- Cedar policies govern what the scanning agent is allowed to do:
432
- - **CAN**: scan code, report findings internally
433
- - **CANNOT**: disclose externally or deploy patches without human approval
434
- - **MUST**: escalate critical findings to humans
435
-
436
- See the [security vulnerability disclosure example](https://github.com/ScopeBlind/examples/tree/main/security-vulnerability-disclosure) for a complete working implementation with Cedar policies and example receipt chains.
437
-
438
- Related: [Vulnerability Disclosure Receipt Design](https://github.com/scopeblind/scopeblind-gateway/issues/2)
439
-
440
- ## Examples
441
-
442
- See complete working examples at [github.com/ScopeBlind/examples](https://github.com/ScopeBlind/examples):
443
- - [Claude Code hooks](https://github.com/ScopeBlind/examples/tree/main/claude-code-hooks) — receipt signing for every tool call
444
- - [Security vulnerability disclosure](https://github.com/ScopeBlind/examples/tree/main/security-vulnerability-disclosure) — receipt-signed disclosure lifecycle with Cedar governance
445
- - [MCP server signing](https://github.com/ScopeBlind/examples/tree/main/mcp-server-signing) — Cedar WASM policy engine with audit bundles
446
-
447
- ## ScopeBlind Dashboard
448
-
449
- protect-mcp works fully offline, forever, for free. For teams that want visibility across agents, ScopeBlind offers a hosted dashboard:
450
-
451
- ```bash
452
- npx protect-mcp connect
453
- ```
454
-
455
- | | Free | Pro | Enterprise |
456
- |---|---|---|---|
457
- | Receipts/month | 20,000 | Pay-as-you-go | Annual commit |
458
- | Price | $0 | $0.50 / 1K | $0.40 / 1K |
459
- | Receipt explorer | Yes | Yes | Yes |
460
- | Compliance reports | Yes | Yes | Yes |
461
- | SSO / SAML | - | - | Yes |
462
- | SLA | - | - | 99.9% |
463
-
464
- No signup required for free tier. No card upfront.
465
-
466
- [Dashboard](https://scopeblind.com) | [Docs](https://scopeblind.com/docs/protect-mcp) | [Pricing](https://scopeblind.com/pricing)
467
-
468
- ## Telemetry
469
-
470
- protect-mcp sends a single anonymous install beacon on first run (package name, version, OS, Node version). No PII. Disable with:
471
-
472
- ```bash
473
- PROTECT_MCP_TELEMETRY=off
474
- ```
201
+ Run `npx protect-mcp --help` for the full flag reference.
475
202
 
476
- ## License
203
+ ## Links
477
204
 
478
- MIT free to use, modify, distribute, and build upon without restriction.
205
+ - Protocol (IETF): [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
206
+ - [CHANGELOG](./CHANGELOG.md)
207
+ - [npm](https://www.npmjs.com/package/protect-mcp)
208
+ - [scopeblind.com](https://scopeblind.com)
479
209
 
480
- Built by [ScopeBlind](https://scopeblind.com) | [npm](https://www.npmjs.com/package/protect-mcp) | [GitHub](https://github.com/scopeblind/scopeblind-gateway) | [IETF Draft](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
210
+ MIT licensed. Built by [ScopeBlind](https://scopeblind.com).
package/SECURITY.md ADDED
@@ -0,0 +1,48 @@
1
+ # Security Policy
2
+
3
+ This policy covers the `protect-mcp` npm package.
4
+
5
+ ## Supported versions
6
+
7
+ | Version | Supported |
8
+ |---------|-----------|
9
+ | >= 0.7.0 | Yes |
10
+ | 0.6.x | No, upgrade |
11
+ | 0.5.x | No, upgrade |
12
+
13
+ ## Affected versions
14
+
15
+ The 0.5.x and 0.6.x lines have a fail-open gate: the Cedar policy was not
16
+ evaluated correctly against the pinned engine, and the evaluator returned ALLOW
17
+ on evaluation error. A `forbid` rule could therefore fail to block. This is fixed
18
+ in 0.7.0, which fails closed (denies) on any evaluation error, missing engine, or
19
+ errored policy. If you are on 0.5.x or 0.6.x, upgrade to >= 0.7.0.
20
+
21
+ Advisory: [GHSA-hm46-7j72-rpv9](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/GHSA-hm46-7j72-rpv9).
22
+
23
+ ## Design posture
24
+
25
+ The gate fails closed by default. On any policy error, a missing engine, or an
26
+ evaluation failure, the decision is DENY, never a silent ALLOW. Before arming an
27
+ enforcing gate, `serve --enforce` and `doctor` run a boot self-test that proves
28
+ the gate denies a known-forbidden vector, and refuse to start if it cannot. The
29
+ observe mode that allows on error is opt-in, and even then flags any call that
30
+ would be blocked as `would_deny: true`.
31
+
32
+ ## Reporting a vulnerability
33
+
34
+ Please report security issues privately. Do not open a public issue for an
35
+ unpatched vulnerability.
36
+
37
+ - Email: security@scopeblind.com
38
+ - Or open a private advisory via [GitHub Security Advisories](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/new).
39
+
40
+ Include the affected version, a description, and (if possible) a minimal
41
+ reproduction. We aim to acknowledge reports within 3 business days and to ship a
42
+ fix or a clear remediation plan as quickly as the severity warrants.
43
+
44
+ ## Disclosure and credit
45
+
46
+ We follow coordinated disclosure: we work with you on a fix and a timeline before
47
+ any public detail is released. Reporters who disclose responsibly are credited in
48
+ the advisory and the CHANGELOG unless they ask to remain anonymous.
package/dist/cli.js CHANGED
@@ -7622,17 +7622,65 @@ function loadPolicyArg(argv) {
7622
7622
  }
7623
7623
  return null;
7624
7624
  }
7625
+ async function readHookStdin() {
7626
+ if (process.stdin.isTTY) return null;
7627
+ try {
7628
+ const chunks = [];
7629
+ for await (const chunk of process.stdin) chunks.push(chunk);
7630
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
7631
+ return raw ? JSON.parse(raw) : null;
7632
+ } catch {
7633
+ return null;
7634
+ }
7635
+ }
7636
+ function mapHookPayload(j) {
7637
+ const tool = j.tool_name ?? j.toolName;
7638
+ const input = j.tool_input ?? j.toolInput;
7639
+ if (input === void 0 && j.command !== void 0) {
7640
+ return { tool: tool ?? "Bash", input: { command: j.command } };
7641
+ }
7642
+ return { tool, input };
7643
+ }
7644
+ function emitDecision(format, allowed, reason) {
7645
+ if (format === "hermes") {
7646
+ process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
7647
+ process.exit(0);
7648
+ }
7649
+ if (allowed) {
7650
+ process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
7651
+ process.exit(0);
7652
+ }
7653
+ if (format === "cursor") {
7654
+ process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
7655
+ } else if (format === "gemini") {
7656
+ process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
7657
+ }
7658
+ process.stderr.write(`protect-mcp denied: ${reason}
7659
+ `);
7660
+ process.exit(2);
7661
+ }
7625
7662
  async function handleEvaluate(argv) {
7626
- const tool = flagValue(argv, "--tool") || "";
7627
- const inputRaw = flagValue(argv, "--input") || "{}";
7663
+ const format = flagValue(argv, "--format");
7664
+ let tool = flagValue(argv, "--tool") || "";
7665
+ let inputRaw = flagValue(argv, "--input") || "{}";
7628
7666
  const contextRaw = flagValue(argv, "--context");
7629
7667
  const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
7668
+ if (format) {
7669
+ const j = await readHookStdin();
7670
+ if (j) {
7671
+ const m = mapHookPayload(j);
7672
+ if (m.tool) tool = m.tool;
7673
+ if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
7674
+ }
7675
+ }
7630
7676
  const policySet = loadPolicyArg(argv);
7631
7677
  if (!policySet) {
7632
7678
  if (failOnMissing) {
7679
+ if (format) emitDecision(format, false, "policy not found (fail-closed)");
7633
7680
  process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
7634
7681
  process.exit(2);
7635
7682
  }
7683
+ if (format) emitDecision(format, true, "no_policy_configured");
7636
7684
  process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
7637
7685
  process.exit(0);
7638
7686
  }
@@ -7653,13 +7701,22 @@ async function handleEvaluate(argv) {
7653
7701
  context.command_pattern = input.command;
7654
7702
  }
7655
7703
  const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
7704
+ if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
7656
7705
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
7657
7706
  process.exit(decision.allowed ? 0 : 2);
7658
7707
  }
7659
7708
  async function handleSign(argv) {
7660
- const tool = flagValue(argv, "--tool") || "";
7709
+ const format = flagValue(argv, "--format");
7710
+ let tool = flagValue(argv, "--tool") || "";
7661
7711
  const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
7662
7712
  const keyPath = flagValue(argv, "--key");
7713
+ if (format) {
7714
+ const j = await readHookStdin();
7715
+ if (j) {
7716
+ const m = mapHookPayload(j);
7717
+ if (m.tool) tool = m.tool;
7718
+ }
7719
+ }
7663
7720
  if (keyPath && (0, import_node_fs10.existsSync)(keyPath)) {
7664
7721
  try {
7665
7722
  await initSigning({ enabled: true, key_path: keyPath });
@@ -7685,6 +7742,10 @@ async function handleSign(argv) {
7685
7742
  (0, import_node_fs10.appendFileSync)((0, import_node_path6.join)(receiptsDir, "receipts.jsonl"), line + "\n");
7686
7743
  } catch {
7687
7744
  }
7745
+ if (format === "hermes") {
7746
+ process.stdout.write("{}\n");
7747
+ process.exit(0);
7748
+ }
7688
7749
  process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
7689
7750
  process.exit(0);
7690
7751
  }
package/dist/cli.mjs CHANGED
@@ -1297,17 +1297,65 @@ function loadPolicyArg(argv) {
1297
1297
  }
1298
1298
  return null;
1299
1299
  }
1300
+ async function readHookStdin() {
1301
+ if (process.stdin.isTTY) return null;
1302
+ try {
1303
+ const chunks = [];
1304
+ for await (const chunk of process.stdin) chunks.push(chunk);
1305
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
1306
+ return raw ? JSON.parse(raw) : null;
1307
+ } catch {
1308
+ return null;
1309
+ }
1310
+ }
1311
+ function mapHookPayload(j) {
1312
+ const tool = j.tool_name ?? j.toolName;
1313
+ const input = j.tool_input ?? j.toolInput;
1314
+ if (input === void 0 && j.command !== void 0) {
1315
+ return { tool: tool ?? "Bash", input: { command: j.command } };
1316
+ }
1317
+ return { tool, input };
1318
+ }
1319
+ function emitDecision(format, allowed, reason) {
1320
+ if (format === "hermes") {
1321
+ process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
1322
+ process.exit(0);
1323
+ }
1324
+ if (allowed) {
1325
+ process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
1326
+ process.exit(0);
1327
+ }
1328
+ if (format === "cursor") {
1329
+ process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
1330
+ } else if (format === "gemini") {
1331
+ process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
1332
+ }
1333
+ process.stderr.write(`protect-mcp denied: ${reason}
1334
+ `);
1335
+ process.exit(2);
1336
+ }
1300
1337
  async function handleEvaluate(argv) {
1301
- const tool = flagValue(argv, "--tool") || "";
1302
- const inputRaw = flagValue(argv, "--input") || "{}";
1338
+ const format = flagValue(argv, "--format");
1339
+ let tool = flagValue(argv, "--tool") || "";
1340
+ let inputRaw = flagValue(argv, "--input") || "{}";
1303
1341
  const contextRaw = flagValue(argv, "--context");
1304
1342
  const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
1343
+ if (format) {
1344
+ const j = await readHookStdin();
1345
+ if (j) {
1346
+ const m = mapHookPayload(j);
1347
+ if (m.tool) tool = m.tool;
1348
+ if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
1349
+ }
1350
+ }
1305
1351
  const policySet = loadPolicyArg(argv);
1306
1352
  if (!policySet) {
1307
1353
  if (failOnMissing) {
1354
+ if (format) emitDecision(format, false, "policy not found (fail-closed)");
1308
1355
  process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
1309
1356
  process.exit(2);
1310
1357
  }
1358
+ if (format) emitDecision(format, true, "no_policy_configured");
1311
1359
  process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
1312
1360
  process.exit(0);
1313
1361
  }
@@ -1328,13 +1376,22 @@ async function handleEvaluate(argv) {
1328
1376
  context.command_pattern = input.command;
1329
1377
  }
1330
1378
  const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
1379
+ if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
1331
1380
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
1332
1381
  process.exit(decision.allowed ? 0 : 2);
1333
1382
  }
1334
1383
  async function handleSign(argv) {
1335
- const tool = flagValue(argv, "--tool") || "";
1384
+ const format = flagValue(argv, "--format");
1385
+ let tool = flagValue(argv, "--tool") || "";
1336
1386
  const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
1337
1387
  const keyPath = flagValue(argv, "--key");
1388
+ if (format) {
1389
+ const j = await readHookStdin();
1390
+ if (j) {
1391
+ const m = mapHookPayload(j);
1392
+ if (m.tool) tool = m.tool;
1393
+ }
1394
+ }
1338
1395
  if (keyPath && existsSyncCli(keyPath)) {
1339
1396
  try {
1340
1397
  await initSigning({ enabled: true, key_path: keyPath });
@@ -1360,6 +1417,10 @@ async function handleSign(argv) {
1360
1417
  appendFileSyncCli(joinCli(receiptsDir, "receipts.jsonl"), line + "\n");
1361
1418
  } catch {
1362
1419
  }
1420
+ if (format === "hermes") {
1421
+ process.stdout.write("{}\n");
1422
+ process.exit(0);
1423
+ }
1363
1424
  process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
1364
1425
  process.exit(0);
1365
1426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "mcpName": "com.scopeblind/protect-mcp",
5
5
  "description": "Fail-closed Cedar policy gate + signed receipts for AI agent tool calls. Blocks what breaks the rules before it runs, denies on any policy error, and proves the gate is live with a startup self-test.",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,8 @@
25
25
  "dist",
26
26
  "policies",
27
27
  "README.md",
28
- "CHANGELOG.md"
28
+ "CHANGELOG.md",
29
+ "SECURITY.md"
29
30
  ],
30
31
  "keywords": [
31
32
  "scopeblind",