@ory/claude-code 1.9.1 → 1.10.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 +7 -1
- package/dist/cli/main.js +16 -82
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +13 -11
- package/dist/handlers.d.ts +14 -2
- package/dist/handlers.js +105 -70
- package/dist/hook.d.ts +1 -1
- package/dist/hook.js +22 -92
- package/dist/settings.js +5 -5
- package/dist/types.d.ts +14 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,9 +12,11 @@ One command installs two independent halves:
|
|
|
12
12
|
## What you'll need
|
|
13
13
|
|
|
14
14
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code), installed and signed in
|
|
15
|
+
<!-- BEGIN GENERATED requirements -->
|
|
15
16
|
- Node.js **22 or newer**
|
|
16
17
|
- [Docker](https://docs.docker.com/get-docker/) — only if you want to run Ory locally
|
|
17
18
|
- macOS or Linux (Windows works via WSL2)
|
|
19
|
+
<!-- END GENERATED requirements -->
|
|
18
20
|
|
|
19
21
|
## Get started
|
|
20
22
|
|
|
@@ -105,6 +107,7 @@ Then a denied tool is actually blocked and Claude shows why.
|
|
|
105
107
|
|
|
106
108
|
## Connect to Ory Agent Security
|
|
107
109
|
|
|
110
|
+
<!-- BEGIN GENERATED connect -->
|
|
108
111
|
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
109
112
|
|
|
110
113
|
| Value | Flag | Environment variable |
|
|
@@ -128,6 +131,7 @@ npx -y -p @ory/claude-code ory-claude configure \
|
|
|
128
131
|
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.
|
|
129
132
|
|
|
130
133
|
Settings are saved to `~/.config/ory-agent-plugins/config.json` and shared across all your Ory agent plugins; environment variables win when both are set. Running headless with an OAuth2 access token already? Set `ORY_USER_OAUTH2_TOKEN` and the browser step is skipped entirely.
|
|
134
|
+
<!-- END GENERATED connect -->
|
|
131
135
|
|
|
132
136
|
## Commands
|
|
133
137
|
|
|
@@ -145,14 +149,16 @@ All prefixed with `npx -y -p @ory/claude-code`.
|
|
|
145
149
|
|
|
146
150
|
## Troubleshooting
|
|
147
151
|
|
|
148
|
-
- **`local-up` fails** — make sure Docker is running and ports `4000`, `4100`, and `4455` are free
|
|
152
|
+
- **`local-up` fails** — <!-- BEGIN GENERATED local-stack-ports -->make sure Docker is running and ports `4000`, `4100`, and `4455` are free.<!-- END GENERATED local-stack-ports -->
|
|
149
153
|
- **Browser sign-in loops** (after connecting) — confirm the reserved login client has all four documented loopback redirect URIs; `agent unregister` only clears legacy agent OAuth state.
|
|
150
154
|
- **Running an older CLI than expected** — `npx -p @ory/claude-code` (no version pin) reuses a previously-downloaded copy instead of re-resolving to the latest. The install banner prints the running version; confirm it with `npx -y -p @ory/claude-code ory-claude version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/claude-code ory-claude install`. Pinning an exact version (`@ory/claude-code@<version>`) also bypasses the cached copy.
|
|
151
155
|
- **Hooks stuck on an old version** — run `/plugin marketplace update ory` inside Claude Code, or re-run the installer.
|
|
152
156
|
|
|
153
157
|
## Learn more
|
|
154
158
|
|
|
159
|
+
<!-- BEGIN GENERATED ory-links -->
|
|
155
160
|
- [Ory documentation](https://www.ory.com/docs/) · [Ory Console](https://console.ory.sh) · [Ory Elements](https://github.com/ory/elements)
|
|
161
|
+
<!-- END GENERATED ory-links -->
|
|
156
162
|
- [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code)
|
|
157
163
|
|
|
158
164
|
## License
|
package/dist/cli/main.js
CHANGED
|
@@ -47,100 +47,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
47
47
|
const path = __importStar(require("node:path"));
|
|
48
48
|
const fs = __importStar(require("node:fs"));
|
|
49
49
|
const argus_1 = require("@ory/argus");
|
|
50
|
+
const cli_1 = require("@ory/argus/cli");
|
|
51
|
+
const install_1 = require("@ory/argus/install");
|
|
50
52
|
const setup_js_1 = require("./setup.js");
|
|
51
53
|
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
52
54
|
/** Resolved per call: see the note on `persistentDir()` in ./setup.ts. */
|
|
53
55
|
function persistentDir() {
|
|
54
56
|
return (0, argus_1.getHarnessDataDir)("claude-code");
|
|
55
57
|
}
|
|
56
|
-
function main() {
|
|
57
|
-
const [command, ...args] = process.argv.slice(2);
|
|
58
|
-
// `--help` after a matched subcommand: print usage, do nothing else.
|
|
59
|
-
// Without this the switch below ignores the flag and `install --help`
|
|
60
|
-
// performs a real install (#221).
|
|
61
|
-
if ((0, argus_1.shouldPrintHelp)(command, args))
|
|
62
|
-
return help();
|
|
63
|
-
switch (command) {
|
|
64
|
-
case "install":
|
|
65
|
-
if (args.includes("--print")) {
|
|
66
|
-
(0, setup_js_1.printInstallPlan)(args);
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
(0, argus_1.beginDeferNextSteps)();
|
|
70
|
-
(0, setup_js_1.install)(args);
|
|
71
|
-
(0, argus_1.runPostInstall)("ory-claude", "claude-code", args).then(() => process.exit(0), (err) => {
|
|
72
|
-
console.error(err.message ?? err);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
});
|
|
75
|
-
break;
|
|
76
|
-
case "uninstall":
|
|
77
|
-
(0, setup_js_1.uninstall)(args);
|
|
78
|
-
(0, argus_1.clearCredentialsForUninstall)({
|
|
79
|
-
harness: "claude-code",
|
|
80
|
-
purge: args.includes("--purge"),
|
|
81
|
-
}).then(() => process.exit(0), (err) => {
|
|
82
|
-
console.error(err.message ?? err);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
});
|
|
85
|
-
break;
|
|
86
|
-
case "configure":
|
|
87
|
-
(0, argus_1.runConfigureCommand)("ory-claude", args);
|
|
88
|
-
break;
|
|
89
|
-
case "agent":
|
|
90
|
-
(0, argus_1.runAgentCommand)("ory-claude", "claude-code", args).then((code) => process.exit(code), (err) => {
|
|
91
|
-
console.error(err.message ?? err);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
});
|
|
94
|
-
break;
|
|
95
|
-
case "permissions":
|
|
96
|
-
(0, argus_1.runPermissionsCommand)("ory-claude", "claude-code", args).then((code) => process.exit(code), (err) => {
|
|
97
|
-
console.error(err.message ?? err);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
});
|
|
100
|
-
break;
|
|
101
|
-
case "status":
|
|
102
|
-
status().then(() => process.exit(0), (err) => {
|
|
103
|
-
console.error(err.message ?? err);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
});
|
|
106
|
-
break;
|
|
107
|
-
case "watch":
|
|
108
|
-
(0, argus_1.runWatchCommand)("ory-claude", "claude-code", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
109
|
-
break;
|
|
110
|
-
case "local":
|
|
111
|
-
(0, argus_1.runLocalCommand)("ory-claude", args).catch((err) => {
|
|
112
|
-
console.error(err.message ?? err);
|
|
113
|
-
process.exit(1);
|
|
114
|
-
});
|
|
115
|
-
break;
|
|
116
|
-
case "version":
|
|
117
|
-
case "--version":
|
|
118
|
-
case "-v":
|
|
119
|
-
process.exit((0, argus_1.runVersionCommand)("ory-claude", args, { packageRoot: PACKAGE_ROOT }));
|
|
120
|
-
break;
|
|
121
|
-
case "help":
|
|
122
|
-
case "--help":
|
|
123
|
-
case "-h":
|
|
124
|
-
case undefined:
|
|
125
|
-
help();
|
|
126
|
-
break;
|
|
127
|
-
default:
|
|
128
|
-
console.error(`Unknown command: ${command}`);
|
|
129
|
-
help();
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
58
|
async function status() {
|
|
134
|
-
await (0,
|
|
59
|
+
await (0, cli_1.runStatusCommand)("ory-claude", "claude-code", {
|
|
135
60
|
title: "Claude Code",
|
|
136
61
|
printPluginSection: () => {
|
|
137
62
|
// A wiped data dir leaves the harness pointing at a marketplace root
|
|
138
63
|
// that no longer exists, and its plugin surface then fails with an error
|
|
139
64
|
// that never mentions Ory. Say so here, with the repair (#206).
|
|
140
|
-
const dangling = (0,
|
|
65
|
+
const dangling = (0, install_1.findDanglingRegistration)("claude-code");
|
|
141
66
|
if (dangling) {
|
|
142
67
|
console.log("");
|
|
143
|
-
console.log(` ! ${(0,
|
|
68
|
+
console.log(` ! ${(0, install_1.describeDanglingRegistration)(dangling)}`);
|
|
144
69
|
console.log("");
|
|
145
70
|
}
|
|
146
71
|
console.log("Hooks & plugin:");
|
|
@@ -148,7 +73,7 @@ async function status() {
|
|
|
148
73
|
// install time, so the wired code and this CLI's own version can
|
|
149
74
|
// legitimately differ — report the runtime, not our own version. Run
|
|
150
75
|
// `claude plugin list` for the `claude` CLI's view of registrations.
|
|
151
|
-
console.log(` Runtime: ${(0,
|
|
76
|
+
console.log(` Runtime: ${(0, install_1.describeRuntimeHealth)((0, install_1.checkRuntimeHealth)("claude-code"))}`);
|
|
152
77
|
// The data dir doubles as the default activity/debug-log home, so it exists
|
|
153
78
|
// after any session regardless of install state — its mere presence does
|
|
154
79
|
// NOT mean a plugin was assembled here. `isPluginAssembled` keys off the
|
|
@@ -190,4 +115,13 @@ Examples:
|
|
|
190
115
|
npx -y -p @ory/claude-code ory-claude uninstall # Remove plugin
|
|
191
116
|
`);
|
|
192
117
|
}
|
|
193
|
-
|
|
118
|
+
(0, cli_1.runHarnessCli)({
|
|
119
|
+
bin: "ory-claude",
|
|
120
|
+
harness: "claude-code",
|
|
121
|
+
packageRoot: PACKAGE_ROOT,
|
|
122
|
+
help,
|
|
123
|
+
install: setup_js_1.install,
|
|
124
|
+
printInstallPlan: setup_js_1.printInstallPlan,
|
|
125
|
+
uninstall: setup_js_1.uninstall,
|
|
126
|
+
status: () => status(),
|
|
127
|
+
});
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* cannot. The public `ory/claude-plugins` mirror stays a valid install route
|
|
18
18
|
* on its own terms (it ships a bootstrap keyed off `${CLAUDE_PLUGIN_ROOT}`).
|
|
19
19
|
*/
|
|
20
|
-
import { type PreparedRuntime } from "@ory/argus";
|
|
20
|
+
import { type PreparedRuntime } from "@ory/argus/install";
|
|
21
21
|
/**
|
|
22
22
|
* Assemble the Claude Code plugin directory in the persistent location and
|
|
23
23
|
* return its path (the marketplace root).
|
package/dist/cli/setup.js
CHANGED
|
@@ -61,6 +61,8 @@ const fs = __importStar(require("node:fs"));
|
|
|
61
61
|
const path = __importStar(require("node:path"));
|
|
62
62
|
const claude_command_js_1 = require("./claude-command.js");
|
|
63
63
|
const argus_1 = require("@ory/argus");
|
|
64
|
+
const cli_1 = require("@ory/argus/cli");
|
|
65
|
+
const install_1 = require("@ory/argus/install");
|
|
64
66
|
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
65
67
|
const PACKAGE_NAME = "@ory/claude-code";
|
|
66
68
|
const HARNESS = "claude-code";
|
|
@@ -154,13 +156,13 @@ function assemblePluginDir(runtime) {
|
|
|
154
156
|
// Skills — materialized fresh from the shared core templates.
|
|
155
157
|
const skillsDir = path.join(persistentDir(), "skills");
|
|
156
158
|
fs.rmSync(skillsDir, { recursive: true, force: true });
|
|
157
|
-
(0,
|
|
159
|
+
(0, install_1.writeSkillTree)(skillsDir, (0, install_1.renderOrySkills)("claude-code", RENDER_OPTS));
|
|
158
160
|
// Commands — plain markdown command files (Claude Code's `/ory:<name>`).
|
|
159
161
|
const commandsDir = path.join(persistentDir(), "commands");
|
|
160
162
|
fs.rmSync(commandsDir, { recursive: true, force: true });
|
|
161
163
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
162
|
-
for (const cmd of (0,
|
|
163
|
-
fs.writeFileSync(path.join(commandsDir, `${cmd.slug}.md`), (0,
|
|
164
|
+
for (const cmd of (0, install_1.renderOryCommands)("claude-code", RENDER_OPTS)) {
|
|
165
|
+
fs.writeFileSync(path.join(commandsDir, `${cmd.slug}.md`), (0, install_1.commandToPlainMarkdown)(cmd));
|
|
164
166
|
}
|
|
165
167
|
// MCP server — points at the shim, so the server starts from the resolved
|
|
166
168
|
// runtime. Omitted entirely when this runtime ships no MCP server, rather
|
|
@@ -182,7 +184,7 @@ function assemblePluginDir(runtime) {
|
|
|
182
184
|
// window the interactive login needs, so leaving it undeclared let the hook
|
|
183
185
|
// be killed mid-sign-in (#220). Both values come from core so they cannot
|
|
184
186
|
// drift from the login timeout they have to outlast.
|
|
185
|
-
const command = (0,
|
|
187
|
+
const command = (0, install_1.requireHookCommand)(runtime);
|
|
186
188
|
const entry = (timeout) => [
|
|
187
189
|
{ matcher: "", hooks: [{ type: "command", command, timeout }] },
|
|
188
190
|
];
|
|
@@ -249,7 +251,7 @@ function install(args) {
|
|
|
249
251
|
process.exit(1);
|
|
250
252
|
}
|
|
251
253
|
installAssembled(scope, args);
|
|
252
|
-
(0,
|
|
254
|
+
(0, install_1.printNextSteps)("Claude Code", "npx -y -p @ory/claude-code ory-claude uninstall", {
|
|
253
255
|
binName: "ory-claude",
|
|
254
256
|
harness: HARNESS,
|
|
255
257
|
});
|
|
@@ -268,7 +270,7 @@ function printInstallPlan(args) {
|
|
|
268
270
|
* tree with the `claude` CLI as a local-file marketplace.
|
|
269
271
|
*/
|
|
270
272
|
function installAssembled(scope, args) {
|
|
271
|
-
const runtime = (0,
|
|
273
|
+
const runtime = (0, install_1.wireRuntime)({
|
|
272
274
|
harness: HARNESS,
|
|
273
275
|
packageName: PACKAGE_NAME,
|
|
274
276
|
packageRoot: PACKAGE_ROOT,
|
|
@@ -334,7 +336,7 @@ function installAssembled(scope, args) {
|
|
|
334
336
|
// Claude Code stores this marketplace path and re-reads it, so a data-dir
|
|
335
337
|
// wipe that skips uninstall would leave it pointing at nothing. Record how to
|
|
336
338
|
// undo the registration so the destructive paths can replay it (#206).
|
|
337
|
-
(0,
|
|
339
|
+
(0, install_1.recordExternalRegistration)({
|
|
338
340
|
harness: "claude-code",
|
|
339
341
|
tool: "claude",
|
|
340
342
|
description: `Claude Code marketplace "${marketplaceName}"`,
|
|
@@ -385,12 +387,12 @@ function uninstall(args) {
|
|
|
385
387
|
console.log(" Marketplace removed.");
|
|
386
388
|
}
|
|
387
389
|
// Deregistered above, so nothing is left to undo.
|
|
388
|
-
(0,
|
|
390
|
+
(0, install_1.clearExternalRegistration)("claude-code");
|
|
389
391
|
cleanPersistentDir();
|
|
390
392
|
// Drop the shim and GC the runtime store, unless another harness still
|
|
391
393
|
// points at it.
|
|
392
|
-
(0,
|
|
393
|
-
for (const pruned of (0,
|
|
394
|
+
(0, install_1.removeRuntimeWiring)(HARNESS);
|
|
395
|
+
for (const pruned of (0, install_1.pruneRuntimeStores)()) {
|
|
394
396
|
console.log(` Removed runtime: ${pruned}`);
|
|
395
397
|
}
|
|
396
398
|
}
|
|
@@ -422,7 +424,7 @@ with the 'claude' CLI as a local-file marketplace.
|
|
|
422
424
|
uninstall(args);
|
|
423
425
|
// Direct `-setup --uninstall` path: also clear stored Ory credentials.
|
|
424
426
|
// (The `ory-claude uninstall` command handles this itself.)
|
|
425
|
-
(0,
|
|
427
|
+
(0, cli_1.clearCredentialsForUninstall)({
|
|
426
428
|
harness: "claude-code",
|
|
427
429
|
purge: process.argv.includes("--purge"),
|
|
428
430
|
}).then(() => process.exit(0), (err) => {
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { ensureAgentIdentity, ensureSubAgentIdentity, ensureUserAuthenticated } from "@ory/argus";
|
|
2
|
-
import type { OryAgentClient } from "@ory/argus";
|
|
3
2
|
import type { ClaudeCodeHookInput, ClaudeCodeHookOutput } from "./types.js";
|
|
4
3
|
export interface HandleHookEventDeps {
|
|
5
4
|
userLogin?: typeof ensureUserAuthenticated;
|
|
6
5
|
agentGate?: typeof ensureAgentIdentity;
|
|
7
6
|
subAgentGate?: typeof ensureSubAgentIdentity;
|
|
8
7
|
}
|
|
9
|
-
export declare
|
|
8
|
+
export declare const handleHookEvent: import("@ory/argus").HookDispatcher<ClaudeCodeHookInput, ClaudeCodeHookOutput, HandleHookEventDeps>;
|
|
9
|
+
/**
|
|
10
|
+
* Tell the user and the model when the Ory sign-in failed. The session still
|
|
11
|
+
* starts, but without a user identity, enforce mode can deny every tool call.
|
|
12
|
+
* Without this notice nothing would explain those denials. Skipped logins
|
|
13
|
+
* (headless runs, pre-provisioned agents) are expected and stay silent.
|
|
14
|
+
*/
|
|
15
|
+
export declare function signInFailureNotice(userMode: string, userReason: string): ClaudeCodeHookOutput;
|
|
16
|
+
/**
|
|
17
|
+
* Deny a PreToolUse call. Claude Code shows `permissionDecisionReason` to the
|
|
18
|
+
* model, which is how the agent learns why the tool was stopped. This replaces
|
|
19
|
+
* the deprecated top-level `decision: "block"` / `reason` fields.
|
|
20
|
+
*/
|
|
21
|
+
export declare function preToolUseDeny(reason: string): ClaudeCodeHookOutput;
|
package/dist/handlers.js
CHANGED
|
@@ -1,60 +1,51 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleHookEvent =
|
|
3
|
+
exports.handleHookEvent = void 0;
|
|
4
|
+
exports.signInFailureNotice = signInFailureNotice;
|
|
5
|
+
exports.preToolUseDeny = preToolUseDeny;
|
|
4
6
|
const argus_1 = require("@ory/argus");
|
|
5
7
|
const mcp_servers_js_1 = require("./mcp-servers.js");
|
|
6
8
|
const HARNESS = "claude-code";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A tool event fired inside a sub-agent carries that sub-agent's id and type.
|
|
11
|
+
* Restore its runtime credential so the gate authenticates as the sub-agent,
|
|
12
|
+
* not as its parent. Never enrolls: `SubagentStart` owns registration.
|
|
13
|
+
*/
|
|
14
|
+
async function resolveActingSubagent(input, client, deps, event) {
|
|
15
|
+
const activeType = input.agent_type ?? input.subagent_type;
|
|
16
|
+
if (event === "SubagentStart" || !input.agent_id || !activeType || !(0, argus_1.isSecurityConnected)()) {
|
|
17
|
+
return { runtimeCredential: undefined };
|
|
18
|
+
}
|
|
19
|
+
const identity = await (deps.subAgentGate ?? argus_1.ensureSubAgentIdentity)(client, {
|
|
20
|
+
harness: HARNESS,
|
|
11
21
|
sessionId: input.session_id,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let runtimeCredential;
|
|
17
|
-
if (event !== "SubagentStart" && input.agent_id && activeType && (0, argus_1.isSecurityConnected)()) {
|
|
18
|
-
const identity = await (deps.subAgentGate ?? argus_1.ensureSubAgentIdentity)(client, {
|
|
19
|
-
harness: HARNESS,
|
|
20
|
-
sessionId: input.session_id,
|
|
21
|
-
subAgentType: activeType,
|
|
22
|
-
perSpawnId: input.agent_id,
|
|
23
|
-
allowEnrollment: false,
|
|
24
|
-
emitActivity: false,
|
|
25
|
-
});
|
|
26
|
-
if (identity.runtimeCredential) {
|
|
27
|
-
runtimeCredential = identity.runtimeCredential;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return (0, argus_1.withHookContext)(client, { runtimeCredential }, async () => {
|
|
31
|
-
switch (event) {
|
|
32
|
-
case "SessionStart":
|
|
33
|
-
return handleSessionStart(input, client, deps);
|
|
34
|
-
case "PreToolUse":
|
|
35
|
-
return handlePreToolUse(input, client, deps);
|
|
36
|
-
case "PostToolUse":
|
|
37
|
-
return handlePostToolUse(input, client);
|
|
38
|
-
case "PostToolUseFailure":
|
|
39
|
-
return handlePostToolUseFailure(input, client);
|
|
40
|
-
case "PermissionRequest":
|
|
41
|
-
return handlePermissionRequest(input, client);
|
|
42
|
-
case "UserPromptSubmit":
|
|
43
|
-
return handleUserPromptSubmit(input, client);
|
|
44
|
-
case "SubagentStart":
|
|
45
|
-
return handleSubagentStart(input, client, deps);
|
|
46
|
-
case "SubagentStop":
|
|
47
|
-
return handleSubagentStop(input, client);
|
|
48
|
-
case "SessionEnd":
|
|
49
|
-
return handleSessionEnd(input, client);
|
|
50
|
-
default:
|
|
51
|
-
client.logger.debug("hook.passthrough", { event });
|
|
52
|
-
client.logger.activity("hook.passthrough", "skipped", { attributes: { event } });
|
|
53
|
-
return { continue: true };
|
|
54
|
-
}
|
|
55
|
-
});
|
|
22
|
+
subAgentType: activeType,
|
|
23
|
+
perSpawnId: input.agent_id,
|
|
24
|
+
allowEnrollment: false,
|
|
25
|
+
emitActivity: false,
|
|
56
26
|
});
|
|
27
|
+
return { runtimeCredential: identity.runtimeCredential };
|
|
57
28
|
}
|
|
29
|
+
const routes = {
|
|
30
|
+
SessionStart: handleSessionStart,
|
|
31
|
+
PreToolUse: handlePreToolUse,
|
|
32
|
+
PostToolUse: handlePostToolUse,
|
|
33
|
+
PostToolUseFailure: handlePostToolUseFailure,
|
|
34
|
+
PermissionRequest: handlePermissionRequest,
|
|
35
|
+
UserPromptSubmit: handleUserPromptSubmit,
|
|
36
|
+
SubagentStart: handleSubagentStart,
|
|
37
|
+
SubagentStop: handleSubagentStop,
|
|
38
|
+
SessionEnd: handleSessionEnd,
|
|
39
|
+
};
|
|
40
|
+
exports.handleHookEvent = (0, argus_1.createHookDispatcher)({
|
|
41
|
+
harness: HARNESS,
|
|
42
|
+
event: (input) => input.hook_event_name,
|
|
43
|
+
sessionId: (input) => input.session_id,
|
|
44
|
+
log: (input) => ({ sessionId: input.session_id, toolName: input.tool_name }),
|
|
45
|
+
context: resolveActingSubagent,
|
|
46
|
+
routes,
|
|
47
|
+
passthrough: () => ({ continue: true }),
|
|
48
|
+
});
|
|
58
49
|
async function handleSessionStart(input, client, deps) {
|
|
59
50
|
client.logger.info("lifecycle.session_start", {
|
|
60
51
|
sessionId: input.session_id,
|
|
@@ -62,7 +53,7 @@ async function handleSessionStart(input, client, deps) {
|
|
|
62
53
|
source: input.source,
|
|
63
54
|
cwd: input.cwd,
|
|
64
55
|
});
|
|
65
|
-
await (0, argus_1.sessionStart)(client, {
|
|
56
|
+
const result = await (0, argus_1.sessionStart)(client, {
|
|
66
57
|
harness: HARNESS,
|
|
67
58
|
sessionId: input.session_id,
|
|
68
59
|
binName: "ory-claude",
|
|
@@ -70,7 +61,27 @@ async function handleSessionStart(input, client, deps) {
|
|
|
70
61
|
agentGate: deps.agentGate,
|
|
71
62
|
activityAttributes: { model: input.model, source: input.source },
|
|
72
63
|
});
|
|
73
|
-
return
|
|
64
|
+
return signInFailureNotice(result.userMode, result.userReason);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Tell the user and the model when the Ory sign-in failed. The session still
|
|
68
|
+
* starts, but without a user identity, enforce mode can deny every tool call.
|
|
69
|
+
* Without this notice nothing would explain those denials. Skipped logins
|
|
70
|
+
* (headless runs, pre-provisioned agents) are expected and stay silent.
|
|
71
|
+
*/
|
|
72
|
+
function signInFailureNotice(userMode, userReason) {
|
|
73
|
+
if (userMode !== "declined" && userMode !== "error")
|
|
74
|
+
return {};
|
|
75
|
+
const reason = userReason.replace(/[.\s]+$/, "");
|
|
76
|
+
return {
|
|
77
|
+
systemMessage: `Ory sign-in did not complete: ${reason}. Tool calls may be denied this session.`,
|
|
78
|
+
hookSpecificOutput: {
|
|
79
|
+
hookEventName: "SessionStart",
|
|
80
|
+
additionalContext: `Ory Agent Security sign-in did not complete for this session (${reason}). ` +
|
|
81
|
+
"If Ory denies a tool call, tell the user that starting a new session and " +
|
|
82
|
+
"signing in may resolve it. Do not retry the denied call.",
|
|
83
|
+
},
|
|
84
|
+
};
|
|
74
85
|
}
|
|
75
86
|
async function handlePreToolUse(input, client, deps) {
|
|
76
87
|
const toolName = input.tool_name ?? "unknown";
|
|
@@ -87,9 +98,21 @@ async function handlePreToolUse(input, client, deps) {
|
|
|
87
98
|
mcpTool: identifyMcpCall(toolName, input) ?? undefined,
|
|
88
99
|
principals: actingSubagentPrincipals(input, client),
|
|
89
100
|
});
|
|
90
|
-
return result.blocked
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
return result.blocked ? preToolUseDeny(result.denialMessage ?? "Ory permission denied") : {};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Deny a PreToolUse call. Claude Code shows `permissionDecisionReason` to the
|
|
105
|
+
* model, which is how the agent learns why the tool was stopped. This replaces
|
|
106
|
+
* the deprecated top-level `decision: "block"` / `reason` fields.
|
|
107
|
+
*/
|
|
108
|
+
function preToolUseDeny(reason) {
|
|
109
|
+
return {
|
|
110
|
+
hookSpecificOutput: {
|
|
111
|
+
hookEventName: "PreToolUse",
|
|
112
|
+
permissionDecision: "deny",
|
|
113
|
+
permissionDecisionReason: reason,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
93
116
|
}
|
|
94
117
|
function identifyMcpCall(toolName, input) {
|
|
95
118
|
return ((0, mcp_servers_js_1.identifyClaudeCodeMcpTool)(toolName, input.cwd) ??
|
|
@@ -155,36 +178,48 @@ async function handlePermissionRequest(input, client) {
|
|
|
155
178
|
mcpTool: mcpTool ?? undefined,
|
|
156
179
|
});
|
|
157
180
|
if (result.kind === "not_connected") {
|
|
158
|
-
return permissionRequestFallback("Ory Agent Security not connected — deferring to user");
|
|
181
|
+
return permissionRequestFallback(client, "Ory Agent Security not connected — deferring to user");
|
|
159
182
|
}
|
|
160
183
|
if (result.kind === "interactive") {
|
|
161
|
-
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
184
|
+
return permissionRequestFallback(client, "Interactive tool — deferring to user");
|
|
162
185
|
}
|
|
163
186
|
if (result.kind === "fail_open") {
|
|
164
|
-
return permissionRequestFallback(permissionFailureReason(result.decision));
|
|
187
|
+
return permissionRequestFallback(client, permissionFailureReason(result.decision));
|
|
165
188
|
}
|
|
166
189
|
if (result.kind === "deny") {
|
|
167
|
-
return
|
|
190
|
+
return permissionRequestDeny(result.denialMessage ?? "Ory permission denied");
|
|
191
|
+
}
|
|
192
|
+
if (result.kind === "observe") {
|
|
193
|
+
// Policy denied the call but observe mode lets it through. Auto-approving
|
|
194
|
+
// here would also skip the user's own approval prompt, so defer to it.
|
|
195
|
+
return permissionRequestFallback(client, "Ory observe mode — denied by policy, deferring to user");
|
|
168
196
|
}
|
|
169
|
-
|
|
170
|
-
? "Ory
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
client.logger.debug("permission_request.allow", {
|
|
198
|
+
reason: mcpTool ? "Ory MCP server permission granted" : "Ory permission granted",
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow" } },
|
|
202
|
+
};
|
|
174
203
|
}
|
|
175
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Deny a PermissionRequest. Claude Code reads the reason from
|
|
206
|
+
* `decision.message` and shows it to the model.
|
|
207
|
+
*/
|
|
208
|
+
function permissionRequestDeny(message) {
|
|
176
209
|
return {
|
|
177
210
|
hookSpecificOutput: {
|
|
178
211
|
hookEventName: "PermissionRequest",
|
|
179
|
-
decision: { behavior },
|
|
180
|
-
message: reason,
|
|
212
|
+
decision: { behavior: "deny", message },
|
|
181
213
|
},
|
|
182
214
|
};
|
|
183
215
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Return no decision so Claude Code shows its normal permission prompt. Claude
|
|
218
|
+
* Code has no field for a fallback reason, so the reason goes to the debug log.
|
|
219
|
+
*/
|
|
220
|
+
function permissionRequestFallback(client, reason) {
|
|
221
|
+
client.logger.debug("permission_request.defer", { reason });
|
|
222
|
+
return {};
|
|
188
223
|
}
|
|
189
224
|
function permissionFailureReason(decision) {
|
|
190
225
|
if (decision.kind !== "fail_open")
|
package/dist/hook.d.ts
CHANGED
package/dist/hook.js
CHANGED
|
@@ -5,102 +5,32 @@
|
|
|
5
5
|
* This script is invoked by Claude Code for each hook event.
|
|
6
6
|
* Input: JSON on stdin
|
|
7
7
|
* Output: JSON on stdout
|
|
8
|
-
* Exit code 2 =
|
|
8
|
+
* Exit code 2 = PreToolUse deny (the reason is in the JSON and on stderr)
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
const argus_1 = require("@ory/argus");
|
|
12
12
|
const handlers_js_1 = require("./handlers.js");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Read all of stdin as a string.
|
|
25
|
-
*
|
|
26
|
-
* Uses event listeners instead of `for await` so that we don't depend on the
|
|
27
|
-
* parent process closing stdin (sending EOF). If no EOF arrives, we resolve
|
|
28
|
-
* after a short idle gap once data has been received.
|
|
29
|
-
*/
|
|
30
|
-
function readStdin() {
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
const chunks = [];
|
|
33
|
-
let resolved = false;
|
|
34
|
-
let idleTimer;
|
|
35
|
-
function cleanup() {
|
|
36
|
-
if (idleTimer)
|
|
37
|
-
clearTimeout(idleTimer);
|
|
38
|
-
process.stdin.off("data", onData);
|
|
39
|
-
process.stdin.off("end", done);
|
|
40
|
-
process.stdin.off("error", onError);
|
|
41
|
-
process.stdin.pause();
|
|
42
|
-
}
|
|
43
|
-
function done() {
|
|
44
|
-
if (!resolved) {
|
|
45
|
-
resolved = true;
|
|
46
|
-
cleanup();
|
|
47
|
-
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function onData(chunk) {
|
|
51
|
-
chunks.push(chunk);
|
|
52
|
-
// Reset idle timer on every chunk. Once data stops arriving for 100 ms
|
|
53
|
-
// we assume the full payload has been delivered.
|
|
54
|
-
if (idleTimer)
|
|
55
|
-
clearTimeout(idleTimer);
|
|
56
|
-
idleTimer = setTimeout(done, 100);
|
|
13
|
+
void (0, argus_1.runHookProcess)({
|
|
14
|
+
harness: "claude-code",
|
|
15
|
+
handle: handlers_js_1.handleHookEvent,
|
|
16
|
+
respond: (output, input) => {
|
|
17
|
+
if (input.hook_event_name === "SessionEnd") {
|
|
18
|
+
// Claude is already shutting down and may cancel hooks that remain alive.
|
|
19
|
+
// Activity enqueueing synchronously persists the event to the durable outbox,
|
|
20
|
+
// so a later hook can deliver it without delaying session termination.
|
|
21
|
+
return { exitNow: true };
|
|
57
22
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
23
|
+
const specific = output.hookSpecificOutput;
|
|
24
|
+
if (specific?.permissionDecision === "deny") {
|
|
25
|
+
// Exit 2 blocks the tool even if Claude Code rejects the JSON. Claude Code
|
|
26
|
+
// gives the model the JSON deny reason when it parses, and stderr otherwise,
|
|
27
|
+
// so the same reason goes to both.
|
|
28
|
+
return {
|
|
29
|
+
stdout: JSON.stringify(output),
|
|
30
|
+
stderr: `${specific.permissionDecisionReason ?? ""}\n`,
|
|
31
|
+
exitCode: 2,
|
|
32
|
+
};
|
|
64
33
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
process.stdin.on("error", onError);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
async function main() {
|
|
71
|
-
client = argus_1.OryAgentClient.fromEnv("claude-code");
|
|
72
|
-
const raw = await readStdin();
|
|
73
|
-
let input;
|
|
74
|
-
try {
|
|
75
|
-
input = JSON.parse(raw);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
79
|
-
process.exitCode = 0; // Don't block on parse errors
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const output = await (0, handlers_js_1.handleHookEvent)(input, client);
|
|
83
|
-
if (input.hook_event_name === "SessionEnd") {
|
|
84
|
-
// Claude is already shutting down and may cancel hooks that remain alive.
|
|
85
|
-
// Activity enqueueing synchronously persists the event to the durable outbox,
|
|
86
|
-
// so a later hook can deliver it without delaying session termination.
|
|
87
|
-
process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
if (output.decision === "block") {
|
|
90
|
-
process.stdout.write(JSON.stringify(output));
|
|
91
|
-
await flushEvents();
|
|
92
|
-
process.exitCode = 2;
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (output.hookSpecificOutput ||
|
|
96
|
-
output.updatedInput ||
|
|
97
|
-
output.decision === "approve") {
|
|
98
|
-
process.stdout.write(JSON.stringify(output));
|
|
99
|
-
}
|
|
100
|
-
await flushEvents();
|
|
101
|
-
}
|
|
102
|
-
main().catch(async (err) => {
|
|
103
|
-
process.stderr.write(`[ory-agent] fatal: ${err}\n`);
|
|
104
|
-
await flushEvents();
|
|
105
|
-
process.exitCode = 0; // Fail open
|
|
34
|
+
return Object.keys(output).length > 0 ? { stdout: JSON.stringify(output) } : {};
|
|
35
|
+
},
|
|
106
36
|
});
|
package/dist/settings.js
CHANGED
|
@@ -37,7 +37,7 @@ exports.generateSettings = generateSettings;
|
|
|
37
37
|
exports.generateInstalledSettings = generateInstalledSettings;
|
|
38
38
|
exports.generateDevSettings = generateDevSettings;
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
40
|
-
const
|
|
40
|
+
const install_1 = require("@ory/argus/install");
|
|
41
41
|
/**
|
|
42
42
|
* Generate the .claude/settings.local.json that configures Ory hooks.
|
|
43
43
|
*/
|
|
@@ -45,10 +45,10 @@ function generateSettings(hookScriptPath) {
|
|
|
45
45
|
const cmd = `node ${hookScriptPath}`;
|
|
46
46
|
return {
|
|
47
47
|
hooks: {
|
|
48
|
-
SessionStart: (0,
|
|
49
|
-
PreToolUse: (0,
|
|
50
|
-
PostToolUse: (0,
|
|
51
|
-
PermissionRequest: (0,
|
|
48
|
+
SessionStart: (0, install_1.matcherHookEntry)(cmd),
|
|
49
|
+
PreToolUse: (0, install_1.matcherHookEntry)(cmd),
|
|
50
|
+
PostToolUse: (0, install_1.matcherHookEntry)(cmd),
|
|
51
|
+
PermissionRequest: (0, install_1.matcherHookEntry)(cmd),
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
54
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -30,29 +30,27 @@ export interface ClaudeCodeHookInput {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Claude Code hook output written to stdout.
|
|
33
|
+
* See: https://code.claude.com/docs/en/hooks
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* uses the structured `hookSpecificOutput.decision.{behavior}` shape below
|
|
37
|
-
* instead — the top-level `decision` is ignored there.
|
|
35
|
+
* `systemMessage` is shown to the user and never to the model. Decisions and
|
|
36
|
+
* model-facing text live in the per-event `hookSpecificOutput`.
|
|
38
37
|
*/
|
|
39
38
|
export interface ClaudeCodeHookOutput {
|
|
40
39
|
continue?: boolean;
|
|
41
|
-
decision?: "block";
|
|
42
|
-
reason?: string;
|
|
43
40
|
stopReason?: string;
|
|
44
41
|
suppressOutput?: boolean;
|
|
45
42
|
systemMessage?: string;
|
|
46
|
-
updatedInput?: Record<string, unknown>;
|
|
47
43
|
hookSpecificOutput?: ClaudeCodeHookSpecificOutput;
|
|
48
44
|
}
|
|
49
45
|
/**
|
|
50
46
|
* Per-event structured output. Field shape depends on `hookEventName`:
|
|
51
|
-
* - PreToolUse: `permissionDecision` + `permissionDecisionReason
|
|
52
|
-
*
|
|
53
|
-
* - PermissionRequest: nested `decision: { behavior,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
47
|
+
* - PreToolUse: `permissionDecision` + `permissionDecisionReason`. The
|
|
48
|
+
* reason is shown to the model for `deny` and the user for `ask`.
|
|
49
|
+
* - PermissionRequest: nested `decision: { behavior, message?, interrupt? }`.
|
|
50
|
+
* `message` is the deny reason shown to the model. Omit `decision` to fall
|
|
51
|
+
* through to Claude Code's normal user prompt.
|
|
52
|
+
* - SessionStart: `additionalContext` is added to the model's context and is
|
|
53
|
+
* not shown to the user.
|
|
56
54
|
*/
|
|
57
55
|
export interface ClaudeCodeHookSpecificOutput {
|
|
58
56
|
hookEventName?: string;
|
|
@@ -61,12 +59,10 @@ export interface ClaudeCodeHookSpecificOutput {
|
|
|
61
59
|
decision?: {
|
|
62
60
|
behavior: "allow" | "deny";
|
|
63
61
|
updatedInput?: Record<string, unknown>;
|
|
64
|
-
|
|
62
|
+
/** Deny only: the reason shown to the model. */
|
|
63
|
+
message?: string;
|
|
64
|
+
/** Deny only: stop Claude after denying. */
|
|
65
|
+
interrupt?: boolean;
|
|
65
66
|
};
|
|
66
|
-
permissionRules?: string[];
|
|
67
67
|
additionalContext?: string;
|
|
68
|
-
/** Set when the plugin needs the agent to prompt the user for config. */
|
|
69
|
-
oryConfigRequired?: boolean;
|
|
70
|
-
message?: string;
|
|
71
|
-
[key: string]: unknown;
|
|
72
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/claude-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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.
|
|
81
|
+
"@ory/argus": "1.10.0"
|
|
82
82
|
},
|
|
83
83
|
"engines": {
|
|
84
84
|
"node": ">=22"
|