@ory/claude-code 1.5.2 → 1.6.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
@@ -78,6 +78,8 @@ Once you connect to Ory Agent Security, two more things happen automatically:
78
78
 
79
79
  If Ory is ever unreachable, the plugin gets out of the way and lets Claude keep working — so it can't lock you out.
80
80
 
81
+ **MCP servers.** Claude Code names MCP tools `mcp__<server>__<tool>`, so each call is checked against both the server and the tool. The plugin reports the name Claude Code uses, and Ory Agent Security matches it to the MCP server it was configured for. For a server configured in the managed `managed-mcp.json`, the project `.mcp.json`, `~/.claude.json` (user or per-project servers), or an installed plugin's `.mcp.json` or `plugin.json` (`mcp__plugin_<plugin>_<server>__<tool>`), the plugin also sends hashed fingerprints of its launch command or URL, so a well-known server is recognized even under a custom name such as `gh`. Servers passed with `--mcp-config` are not visible to hooks and are matched by name only. Because Agent Security never lets a name override a fingerprint, a custom server named after a well-known one is governed as uncatalogued. Only each server's `command`, `args`, and `url` are read, and only their hashes are sent; `env` and `headers` are never read. claude.ai connectors (`mcp__claude_ai_<Name>__<tool>`) and Claude Desktop Cowork connectors named by UUID are matched on the service side from their name and tool. A server Agent Security cannot identify falls under its Uncatalogued servers level. `ReadMcpResourceTool` and `ListMcpResourcesTool` calls that name a server are checked against that server as well as the native tool, so a disabled or No access server's resources cannot be read; listing resources across every server is checked as the native tool only.
82
+
81
83
  ### See what's happening
82
84
 
83
85
  Everything the plugin does is observable out of the box:
@@ -121,7 +123,7 @@ npx -y -p @ory/claude-code ory-claude configure \
121
123
 
122
124
  **Ory Network or OEL.** Either works. For an Ory Network project the URL is `https://<slug>.projects.oryapis.com`; for a self-hosted **Ory Enterprise License** deployment, point `--project-url` at that deployment's base URL and use the sign-in client id from its Agent Security configuration. Everything downstream — sign-in, checks, delegation — is identical.
123
125
 
124
- **There is nothing else for you to create.** The sign-in client, permission model, native-tool blocks, MCP grants, and observe/enforce posture are provisioned in the Console by someone with project access. At session start the plugin loads or enrolls Claude's Talos runtime credential in the OS credential store; tool paths only read it. Installing needs no workspace privilege.
126
+ **There is nothing else for you to create.** The sign-in client, permission model, native-tool blocks, MCP server access, and observe/enforce posture are provisioned in the Console by someone with project access. At session start the plugin loads or enrolls Claude's Talos runtime credential in the OS credential store; tool paths only read it. Installing needs no workspace privilege.
125
127
 
126
128
  Sign-in runs at the start of every session and never blocks — a declined, skipped, or timed-out login simply leaves that session without a user identity.
127
129
 
package/dist/handlers.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleHookEvent = handleHookEvent;
4
4
  const argus_1 = require("@ory/argus");
5
+ const mcp_servers_js_1 = require("./mcp-servers.js");
5
6
  const HARNESS = "claude-code";
6
7
  function handleHookEvent(input, client, deps = {}) {
7
8
  const event = input.hook_event_name;
@@ -83,13 +84,17 @@ async function handlePreToolUse(input, client, deps) {
83
84
  toolName,
84
85
  toolArgs: input.tool_input,
85
86
  subjectFallback: `session:${input.session_id}`,
86
- mcpTool: (0, argus_1.parseClaudeCodeMcpTool)(toolName) ?? undefined,
87
+ mcpTool: identifyMcpCall(toolName, input) ?? undefined,
87
88
  principals: actingSubagentPrincipals(input, client),
88
89
  });
89
90
  return result.blocked
90
91
  ? { decision: "block", reason: result.denialMessage }
91
92
  : {};
92
93
  }
94
+ function identifyMcpCall(toolName, input) {
95
+ return ((0, mcp_servers_js_1.identifyClaudeCodeMcpTool)(toolName, input.cwd) ??
96
+ (0, mcp_servers_js_1.identifyClaudeCodeMcpResource)(toolName, input.tool_input, input.cwd));
97
+ }
93
98
  function actingSubagentPrincipals(input, client) {
94
99
  const type = input.agent_type;
95
100
  if (!input.agent_id || !type)
@@ -141,7 +146,7 @@ async function handlePermissionRequest(input, client) {
141
146
  toolName,
142
147
  toolInput: input.tool_input,
143
148
  });
144
- const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
149
+ const mcpTool = identifyMcpCall(toolName, input);
145
150
  const result = await (0, argus_1.decideTool)(client, {
146
151
  harness: HARNESS,
147
152
  toolName,
@@ -0,0 +1,26 @@
1
+ import { type McpToolIdentifier } from "@ory/argus";
2
+ /**
3
+ * Identify a Claude Code MCP tool call, with fingerprints of how the server is
4
+ * launched or reached when Claude Code's configuration describes it: the
5
+ * managed `managed-mcp.json`, the project `.mcp.json`, the user's
6
+ * `~/.claude.json` (user and per-project servers), and, for plugin servers
7
+ * (`plugin_<plugin>_<server>`), the installed plugin's `.mcp.json` or
8
+ * `plugin.json`. The hook payload carries only `mcp__<server>__<tool>`, so
9
+ * without fingerprints a server configured under an unrecognized name would
10
+ * resolve to the uncatalogued policy. Servers passed with `--mcp-config` are
11
+ * not visible to hooks and are identified by name only. Only each server's
12
+ * `command`, `args`, and `url` are read, and only their hashes leave the
13
+ * process; `env` and `headers` are never read.
14
+ */
15
+ export declare function identifyClaudeCodeMcpTool(toolName: string, cwd: string | undefined, { managedConfigPath }?: {
16
+ managedConfigPath?: string;
17
+ }): McpToolIdentifier | null;
18
+ /**
19
+ * Identify a Claude Code tool call that reads resources from one MCP server,
20
+ * so the server's enablement and No access apply to it as they do to its
21
+ * tools. Listing resources across every server names no server and stays a
22
+ * native tool call only.
23
+ */
24
+ export declare function identifyClaudeCodeMcpResource(toolName: string, toolInput: unknown, cwd: string | undefined, { managedConfigPath }?: {
25
+ managedConfigPath?: string;
26
+ }): McpToolIdentifier | null;
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.identifyClaudeCodeMcpTool = identifyClaudeCodeMcpTool;
7
+ exports.identifyClaudeCodeMcpResource = identifyClaudeCodeMcpResource;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const argus_1 = require("@ory/argus");
12
+ /** `~/.claude.json` holds history too; skip it rather than parse a very large file per call. */
13
+ const MAX_CONFIG_BYTES = 10 * 1024 * 1024;
14
+ /**
15
+ * Identify a Claude Code MCP tool call, with fingerprints of how the server is
16
+ * launched or reached when Claude Code's configuration describes it: the
17
+ * managed `managed-mcp.json`, the project `.mcp.json`, the user's
18
+ * `~/.claude.json` (user and per-project servers), and, for plugin servers
19
+ * (`plugin_<plugin>_<server>`), the installed plugin's `.mcp.json` or
20
+ * `plugin.json`. The hook payload carries only `mcp__<server>__<tool>`, so
21
+ * without fingerprints a server configured under an unrecognized name would
22
+ * resolve to the uncatalogued policy. Servers passed with `--mcp-config` are
23
+ * not visible to hooks and are identified by name only. Only each server's
24
+ * `command`, `args`, and `url` are read, and only their hashes leave the
25
+ * process; `env` and `headers` are never read.
26
+ */
27
+ function identifyClaudeCodeMcpTool(toolName, cwd, { managedConfigPath = managedMcpConfigPath() } = {}) {
28
+ const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
29
+ return mcpTool ? withFingerprints(mcpTool, cwd, managedConfigPath) : null;
30
+ }
31
+ /** Claude Code's tools that read MCP resources, which name their server in the tool input. */
32
+ const RESOURCE_TOOLS = new Set(["ReadMcpResourceTool", "ListMcpResourcesTool"]);
33
+ /**
34
+ * Identify a Claude Code tool call that reads resources from one MCP server,
35
+ * so the server's enablement and No access apply to it as they do to its
36
+ * tools. Listing resources across every server names no server and stays a
37
+ * native tool call only.
38
+ */
39
+ function identifyClaudeCodeMcpResource(toolName, toolInput, cwd, { managedConfigPath = managedMcpConfigPath() } = {}) {
40
+ if (!RESOURCE_TOOLS.has(toolName) || !toolInput || typeof toolInput !== "object")
41
+ return null;
42
+ const server = toolInput.server;
43
+ if (typeof server !== "string" || server.trim() === "")
44
+ return null;
45
+ // Tool names carry the server name with every character outside
46
+ // [A-Za-z0-9_-] replaced (`plugin:figma:figma` becomes `plugin_figma_figma`),
47
+ // so report and look it up in that form.
48
+ const serverName = server.trim().replace(/[^A-Za-z0-9_-]/g, "_");
49
+ return withFingerprints({ serverName, toolName: "", resource: true }, cwd, managedConfigPath);
50
+ }
51
+ function withFingerprints(mcpTool, cwd, managedConfigPath) {
52
+ const source = configuredServerSource(mcpTool.serverName, cwd, managedConfigPath);
53
+ if (!source)
54
+ return mcpTool;
55
+ const fingerprints = (0, argus_1.mcpServerFingerprints)({
56
+ url: typeof source.url === "string" ? source.url : undefined,
57
+ command: typeof source.command === "string" ? source.command : undefined,
58
+ args: Array.isArray(source.args) ? source.args.filter((arg) => typeof arg === "string") : undefined,
59
+ });
60
+ return fingerprints.length > 0 ? { ...mcpTool, fingerprints } : mcpTool;
61
+ }
62
+ function configuredServerSource(server, cwd, managedConfigPath) {
63
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
64
+ const userConfig = readJson(node_path_1.default.join(configDir ?? (0, node_os_1.homedir)(), ".claude.json"));
65
+ const candidates = [
66
+ // Managed servers take precedence: a managed config gives administrators exclusive control.
67
+ managedConfigPath ? mcpServersOf(readJson(managedConfigPath)) : undefined,
68
+ cwd ? mcpServersOf(readJson(node_path_1.default.join(cwd, ".mcp.json"))) : undefined,
69
+ cwd ? userConfig?.projects?.[cwd]?.mcpServers : undefined,
70
+ userConfig?.mcpServers,
71
+ ];
72
+ for (const servers of candidates) {
73
+ const entry = serverEntry(servers, server);
74
+ if (entry)
75
+ return entry;
76
+ }
77
+ return pluginServerSource(server, node_path_1.default.join(configDir ?? node_path_1.default.join((0, node_os_1.homedir)(), ".claude"), "plugins"));
78
+ }
79
+ /**
80
+ * Find a plugin server, which Claude Code names `plugin_<plugin>_<server>`, in
81
+ * the configuration of the installed plugin. Plugin names may contain `_`, so
82
+ * the installed plugin names decide where the server name starts.
83
+ */
84
+ function pluginServerSource(server, pluginsDir) {
85
+ if (!server.startsWith("plugin_"))
86
+ return undefined;
87
+ const installed = readJson(node_path_1.default.join(pluginsDir, "installed_plugins.json"))
88
+ ?.plugins;
89
+ if (!installed || typeof installed !== "object")
90
+ return undefined;
91
+ for (const [id, installs] of Object.entries(installed)) {
92
+ const plugin = id.split("@")[0];
93
+ const prefix = `plugin_${plugin}_`;
94
+ if (!server.startsWith(prefix) || !Array.isArray(installs))
95
+ continue;
96
+ const name = server.slice(prefix.length);
97
+ for (const install of installs) {
98
+ if (typeof install?.installPath !== "string")
99
+ continue;
100
+ const root = install.installPath;
101
+ const manifest = readJson(node_path_1.default.join(root, ".claude-plugin", "plugin.json"));
102
+ const declared = typeof manifest?.mcpServers === "string"
103
+ ? mcpServersOf(readJson(node_path_1.default.resolve(root, manifest.mcpServers)))
104
+ : manifest?.mcpServers;
105
+ const entry = serverEntry(declared, name) ?? serverEntry(mcpServersOf(readJson(node_path_1.default.join(root, ".mcp.json"))), name);
106
+ if (entry)
107
+ return entry;
108
+ }
109
+ }
110
+ return undefined;
111
+ }
112
+ /** The managed MCP configuration Claude Code reads on this platform. */
113
+ function managedMcpConfigPath() {
114
+ switch (process.platform) {
115
+ case "darwin":
116
+ return "/Library/Application Support/ClaudeCode/managed-mcp.json";
117
+ case "win32":
118
+ return node_path_1.default.join(process.env.ProgramFiles ?? "C:\\Program Files", "ClaudeCode", "managed-mcp.json");
119
+ case "linux":
120
+ return "/etc/claude-code/managed-mcp.json";
121
+ default:
122
+ return undefined;
123
+ }
124
+ }
125
+ /** The servers of an MCP config file, which nests them under `mcpServers` or lists them at the top level. */
126
+ function mcpServersOf(config) {
127
+ if (!config || typeof config !== "object")
128
+ return undefined;
129
+ return config.mcpServers ?? config;
130
+ }
131
+ function serverEntry(servers, server) {
132
+ if (!servers || typeof servers !== "object" || !Object.hasOwn(servers, server))
133
+ return undefined;
134
+ const entry = servers[server];
135
+ return entry && typeof entry === "object" ? entry : undefined;
136
+ }
137
+ function readJson(file) {
138
+ try {
139
+ if ((0, node_fs_1.statSync)(file).size > MAX_CONFIG_BYTES)
140
+ return undefined;
141
+ return JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/claude-code",
3
- "version": "1.5.2",
3
+ "version": "1.6.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://github.com/ory/claude-plugins/tree/master/plugins/ory-agent-plugin",
@@ -78,7 +78,7 @@
78
78
  ],
79
79
  "dependencies": {
80
80
  "reova": "^0.7.0",
81
- "@ory/argus": "1.5.2"
81
+ "@ory/argus": "1.6.0"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=22"