@ory/claude-code 0.1.3 → 0.2.0

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/README.md CHANGED
@@ -120,6 +120,8 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
120
120
 
121
121
  ### Enable enforcement
122
122
 
123
+ After install the plugin runs in **observe mode**: every tool call is checked against Ory Permissions, but a deny is recorded as a `permission.observe_deny` audit span and the tool runs anyway. This lets you see what *would* be blocked before turning on hard blocking.
124
+
123
125
  1. **Turn on the user gate.** In your shell:
124
126
 
125
127
  ```bash
@@ -128,19 +130,29 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
128
130
 
129
131
  The next Claude session opens a browser for PKCE login. Subsequent sessions reuse the persisted token until it expires.
130
132
 
131
- 2. **Grant yourself permission to use a tool.** Use the Ory MCP server from inside Claude (*"grant me invoke on the Bash tool"*) or the CLI directly:
133
+ 2. **Bootstrap tuples for the built-in tools.** One idempotent command grants the current user `use` on every tool Claude ships with (Read, Write, Bash, ):
134
+
135
+ ```bash
136
+ npx -y -p @ory/claude-code ory-claude permissions bootstrap
137
+ ```
138
+
139
+ If a user identity is already cached at install time, the installer runs this for you automatically — re-run after adding tools, switching subjects, or changing the namespace.
140
+
141
+ 3. **Check coverage.** `permissions status` probes every tool in the harness's catalog and prints allowed / denied per tool:
132
142
 
133
143
  ```bash
134
- ory create relationship \
135
- --namespace AgentTools \
136
- --object Bash \
137
- --relation invoke \
138
- --subject-id <your-user-subject-id>
144
+ npx -y -p @ory/claude-code ory-claude permissions status
139
145
  ```
140
146
 
141
- Your subject id is printed at the start of every Claude session when `ORY_AGENT_DEBUG=true`.
147
+ Add tuples for any MCP server tools or custom commands by hand, or via the Ory MCP server from inside Claude (*"grant me use on the Bash tool"*).
148
+
149
+ 4. **Promote to enforce.** Once the observe-mode logs look right, switch over:
150
+
151
+ ```bash
152
+ npx -y -p @ory/claude-code ory-claude permissions enforce
153
+ ```
142
154
 
143
- 3. **See a denial.** Pick a tool you didn't grant (or remove the tuple) and ask Claude to use it. The hook blocks the call and Claude shows the denial reason. The decision is recorded as a `tool.block` trace span.
155
+ Denies now block the tool call; Claude shows the denial reason and the decision is recorded as a `tool.block` trace span with `blocked: true`. Switch back any time with `permissions observe`.
144
156
 
145
157
  ## CLI reference
146
158
 
@@ -148,6 +160,7 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
148
160
  npx -y -p @ory/claude-code ory-claude install | uninstall [--global]
149
161
  npx -y -p @ory/claude-code ory-claude configure [--project-url <url>] [--api-key <key>] [--audit-only]
150
162
  npx -y -p @ory/claude-code ory-claude agent <status|unregister> Manage the agent's OAuth2 identity
163
+ npx -y -p @ory/claude-code ory-claude permissions <status|bootstrap|observe|enforce>
151
164
  npx -y -p @ory/claude-code ory-claude local <up|down|status|seed|logs|env|configure|reset>
152
165
  npx -y -p @ory/claude-code ory-claude status
153
166
  ```
@@ -155,7 +168,8 @@ npx -y -p @ory/claude-code ory-claude status
155
168
  Highlights:
156
169
 
157
170
  - `agent status` — show the current persisted DCR identity for the agent.
158
- - `configure --audit-only` — record decisions without blocking; useful for a phased rollout.
171
+ - `permissions observe` / `permissions enforce` switch between "log denies, allow through" (the install default) and "block denies." `permissions bootstrap` writes `use` tuples for the harness's built-in tools so the promotion path doesn't require hand-writing relationships.
172
+ - `configure --audit-only` — kill switch that disables Ory entirely (no auth, no permission checks; only audit logging of tool invocations). For phased rollouts, prefer `permissions observe` over `--audit-only`.
159
173
  - `local seed` / `local env` — reseed the test user, or print env vars for pointing other tools at the local stack.
160
174
 
161
175
  ## Troubleshooting
package/dist/cli/main.js CHANGED
@@ -55,6 +55,10 @@ function main() {
55
55
  switch (command) {
56
56
  case "install":
57
57
  (0, setup_js_1.install)(args);
58
+ postInstallPermissions("ory-claude", "claude-code").then(() => process.exit(0), (err) => {
59
+ console.error(err.message ?? err);
60
+ process.exit(1);
61
+ });
58
62
  break;
59
63
  case "uninstall":
60
64
  (0, setup_js_1.uninstall)(args);
@@ -68,6 +72,12 @@ function main() {
68
72
  process.exit(1);
69
73
  });
70
74
  break;
75
+ case "permissions":
76
+ (0, argus_1.runPermissionsCommand)("ory-claude", "claude-code", args).then((code) => process.exit(code), (err) => {
77
+ console.error(err.message ?? err);
78
+ process.exit(1);
79
+ });
80
+ break;
71
81
  case "status":
72
82
  status();
73
83
  break;
@@ -89,6 +99,12 @@ function main() {
89
99
  process.exit(1);
90
100
  }
91
101
  }
102
+ async function postInstallPermissions(binName, harness) {
103
+ const bootstrapped = await (0, argus_1.maybeAutoBootstrap)(binName, harness);
104
+ (0, argus_1.printPermissionsOnboardingHelp)(binName, harness, {
105
+ bootstrappedAutomatically: bootstrapped,
106
+ });
107
+ }
92
108
  function status() {
93
109
  console.log("Ory Agent Plugin Status (Claude Code)");
94
110
  console.log("======================================");
@@ -116,6 +132,7 @@ Commands:
116
132
  install [--global] Install Ory plugin via claude CLI marketplace
117
133
  uninstall [--global] Remove Ory plugin and marketplace
118
134
  configure Set or view Ory project URL and API key
135
+ permissions <cmd> Manage permission mode and tool tuples (status, bootstrap, observe, enforce)
119
136
  status Show plugin status and configuration
120
137
  local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
121
138
 
package/dist/handlers.js CHANGED
@@ -188,40 +188,65 @@ async function handlePreToolUse(input, client, deps = {}) {
188
188
  subject,
189
189
  spanAttributes: { toolName },
190
190
  });
191
- if (!mcpResult.allowed) {
191
+ const mcpAttrs = {
192
+ toolName,
193
+ mcpServer: mcpTool.serverName,
194
+ mcpTool: mcpTool.toolName,
195
+ ...inputSummary,
196
+ };
197
+ const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
198
+ object: mcpTool.serverName,
199
+ relation: "use",
200
+ subjectId,
201
+ spanAttributes: mcpAttrs,
202
+ });
203
+ if (decision.kind === "allow") {
204
+ client.tracer.record("tool.invoke", "ok", { attributes: mcpAttrs });
205
+ return {};
206
+ }
207
+ if (decision.kind === "observe") {
192
208
  client.tracer.record("tool.block", "denied", {
193
- attributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary, ...(0, argus_1.alertAttributes)(true) },
209
+ attributes: { ...mcpAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
210
+ });
211
+ client.tracer.record("tool.invoke", "ok", {
212
+ attributes: { ...mcpAttrs, allowed: false, observed: true },
194
213
  });
195
- return {
196
- decision: "block",
197
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
198
- };
214
+ return {};
199
215
  }
200
- client.tracer.record("tool.invoke", "ok", {
201
- attributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary },
202
- });
203
- return {};
204
- }
205
- const namespace = resolveNamespace();
206
- const result = await client.checkPermission({
207
- namespace,
208
- object: toolName,
209
- relation: "use",
210
- ...subject,
211
- }, { spanAttributes: { toolName } });
212
- if (!result.allowed) {
213
216
  client.tracer.record("tool.block", "denied", {
214
- attributes: { toolName, ...inputSummary, allowed: result.allowed, ...(0, argus_1.alertAttributes)(true) },
217
+ attributes: { ...mcpAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
215
218
  });
216
219
  return {
217
220
  decision: "block",
218
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
221
+ reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
219
222
  };
220
223
  }
221
- client.tracer.record("tool.invoke", "ok", {
222
- attributes: { toolName, ...inputSummary, allowed: result.allowed },
224
+ const namespace = resolveNamespace();
225
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace, object: toolName, relation: "use", ...subject }, { spanAttributes: { toolName } });
226
+ if (decision.kind === "fail_open") {
227
+ return handlePermissionError(decision.error, toolName, client);
228
+ }
229
+ const attrs = { toolName, ...inputSummary };
230
+ if (decision.kind === "allow") {
231
+ client.tracer.record("tool.invoke", "ok", { attributes: { ...attrs, allowed: true } });
232
+ return {};
233
+ }
234
+ if (decision.kind === "observe") {
235
+ client.tracer.record("tool.block", "denied", {
236
+ attributes: { ...attrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
237
+ });
238
+ client.tracer.record("tool.invoke", "ok", {
239
+ attributes: { ...attrs, allowed: false, observed: true },
240
+ });
241
+ return {};
242
+ }
243
+ client.tracer.record("tool.block", "denied", {
244
+ attributes: { ...attrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
223
245
  });
224
- return {};
246
+ return {
247
+ decision: "block",
248
+ reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
249
+ };
225
250
  }
226
251
  catch (err) {
227
252
  return handlePermissionError(err, toolName, client);
@@ -354,20 +379,34 @@ async function handlePermissionRequest(input, client) {
354
379
  subject,
355
380
  spanAttributes: { toolName },
356
381
  });
357
- return permissionRequestDecision(mcpResult.allowed ? "allow" : "deny", mcpResult.allowed
358
- ? "Ory MCP server permission granted"
359
- : (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
382
+ const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
383
+ object: mcpTool.serverName,
384
+ relation: "use",
385
+ subjectId,
386
+ spanAttributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
387
+ });
388
+ if (decision.kind === "deny") {
389
+ return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
390
+ }
391
+ return permissionRequestDecision("allow", decision.kind === "observe"
392
+ ? "Ory observe mode — denied by policy but allowed through"
393
+ : "Ory MCP server permission granted");
360
394
  }
361
395
  const namespace = resolveNamespace();
362
- const result = await client.checkPermission({
363
- namespace,
364
- object: toolName,
365
- relation: "use",
366
- ...subject,
367
- }, { spanAttributes: { toolName } });
368
- return permissionRequestDecision(result.allowed ? "allow" : "deny", result.allowed
369
- ? "Ory permission granted"
370
- : (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
396
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace, object: toolName, relation: "use", ...subject }, { spanAttributes: { toolName } });
397
+ if (decision.kind === "fail_open") {
398
+ const code = decision.error.code;
399
+ const fallbackReason = code === "network_error" || code === "rate_limited"
400
+ ? `Ory unavailable (${code}), falling back to user prompt`
401
+ : `Ory permission check failed: ${decision.error.message}`;
402
+ return permissionRequestFallback(fallbackReason);
403
+ }
404
+ if (decision.kind === "deny") {
405
+ return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
406
+ }
407
+ return permissionRequestDecision("allow", decision.kind === "observe"
408
+ ? "Ory observe mode — denied by policy but allowed through"
409
+ : "Ory permission granted");
371
410
  }
372
411
  catch (err) {
373
412
  const oryErr = err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/claude-code",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Ory plugin for Claude Code: scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",
@@ -72,7 +72,7 @@
72
72
  "!dist/**/*.tsbuildinfo"
73
73
  ],
74
74
  "dependencies": {
75
- "@ory/argus": "0.1.3"
75
+ "@ory/argus": "0.2.0"
76
76
  },
77
77
  "engines": {
78
78
  "node": ">=24"