codiedev 0.9.39 → 0.9.41

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.
@@ -0,0 +1,34 @@
1
+ interface CommandHandler {
2
+ type?: unknown;
3
+ command?: unknown;
4
+ commandWindows?: unknown;
5
+ command_windows?: unknown;
6
+ timeout?: unknown;
7
+ async?: unknown;
8
+ statusMessage?: unknown;
9
+ additionalContextLimit?: unknown;
10
+ }
11
+ interface MatcherGroup {
12
+ matcher?: unknown;
13
+ hooks?: unknown;
14
+ }
15
+ export interface CodexHookTrustEntry {
16
+ key: string;
17
+ eventName: string;
18
+ command: string;
19
+ currentHash: string;
20
+ }
21
+ /** Codex hashes a canonical, normalized hook identity before it will execute a
22
+ * user hook. Keep this compatible with codex_config::version_for_toml. */
23
+ export declare function codexCommandHookHash(eventName: string, group: MatcherGroup, handler: CommandHandler): string;
24
+ export declare function codiedevCodexHookTrustEntries(hooksPath: string, hooksFile: unknown): CodexHookTrustEntry[];
25
+ /** `codiedev connect` is an explicit install action, so trust only the exact
26
+ * Codiedev commands it just wrote. Any later command change produces a new
27
+ * hash and Codex disables that hook until connect is run again. */
28
+ export declare function writeCodexHookTrust(configPath: string, hooksPath: string, hooksFile: unknown): CodexHookTrustEntry[];
29
+ export declare function inspectCodexHookTrust(configPath: string, hooksPath: string, hooksFile: unknown, requiredSubcommands?: string[]): {
30
+ ok: boolean;
31
+ total: number;
32
+ trusted: number;
33
+ };
34
+ export {};
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.codexCommandHookHash = codexCommandHookHash;
37
+ exports.codiedevCodexHookTrustEntries = codiedevCodexHookTrustEntries;
38
+ exports.writeCodexHookTrust = writeCodexHookTrust;
39
+ exports.inspectCodexHookTrust = inspectCodexHookTrust;
40
+ const crypto = __importStar(require("crypto"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const detection_1 = require("./detection");
44
+ const TRUST_BEGIN = "# codiedev-hook-trust:begin";
45
+ const TRUST_END = "# codiedev-hook-trust:end";
46
+ const EVENT_LABELS = {
47
+ PreToolUse: "pre_tool_use",
48
+ PermissionRequest: "permission_request",
49
+ PostToolUse: "post_tool_use",
50
+ PreCompact: "pre_compact",
51
+ PostCompact: "post_compact",
52
+ SessionStart: "session_start",
53
+ SessionEnd: "session_end",
54
+ UserPromptSubmit: "user_prompt_submit",
55
+ SubagentStart: "subagent_start",
56
+ SubagentStop: "subagent_stop",
57
+ Stop: "stop",
58
+ };
59
+ const MATCHER_EVENTS = new Set([
60
+ "PreToolUse",
61
+ "PermissionRequest",
62
+ "PostToolUse",
63
+ "PreCompact",
64
+ "PostCompact",
65
+ "SessionStart",
66
+ "SessionEnd",
67
+ "SubagentStart",
68
+ "SubagentStop",
69
+ ]);
70
+ function canonicalize(value) {
71
+ if (Array.isArray(value))
72
+ return value.map(canonicalize);
73
+ if (value && typeof value === "object") {
74
+ return Object.fromEntries(Object.entries(value)
75
+ .sort(([left], [right]) => left.localeCompare(right))
76
+ .map(([key, child]) => [key, canonicalize(child)]));
77
+ }
78
+ return value;
79
+ }
80
+ function normalizedTimeout(eventName, value) {
81
+ const configured = typeof value === "number" && Number.isFinite(value)
82
+ ? Math.floor(value)
83
+ : undefined;
84
+ if (eventName === "SessionEnd")
85
+ return Math.min(3, Math.max(1, configured ?? 1));
86
+ return Math.max(1, configured ?? 600);
87
+ }
88
+ /** Codex hashes a canonical, normalized hook identity before it will execute a
89
+ * user hook. Keep this compatible with codex_config::version_for_toml. */
90
+ function codexCommandHookHash(eventName, group, handler) {
91
+ const normalizedHandler = {
92
+ type: "command",
93
+ command: String(handler.command ?? ""),
94
+ timeout: normalizedTimeout(eventName, handler.timeout),
95
+ async: handler.async === true,
96
+ };
97
+ const commandWindows = typeof handler.commandWindows === "string"
98
+ ? handler.commandWindows
99
+ : typeof handler.command_windows === "string"
100
+ ? handler.command_windows
101
+ : undefined;
102
+ if (commandWindows !== undefined)
103
+ normalizedHandler.commandWindows = commandWindows;
104
+ if (typeof handler.statusMessage === "string")
105
+ normalizedHandler.statusMessage = handler.statusMessage;
106
+ if (typeof handler.additionalContextLimit === "number") {
107
+ normalizedHandler.additionalContextLimit = handler.additionalContextLimit;
108
+ }
109
+ const identity = {
110
+ event_name: EVENT_LABELS[eventName],
111
+ hooks: [normalizedHandler],
112
+ };
113
+ if (MATCHER_EVENTS.has(eventName) && typeof group.matcher === "string") {
114
+ identity.matcher = group.matcher;
115
+ }
116
+ const serialized = JSON.stringify(canonicalize(identity));
117
+ return `sha256:${crypto.createHash("sha256").update(serialized).digest("hex")}`;
118
+ }
119
+ function normalizedHooksPath(hooksPath) {
120
+ try {
121
+ return fs.realpathSync.native(hooksPath);
122
+ }
123
+ catch {
124
+ return path.resolve(hooksPath);
125
+ }
126
+ }
127
+ function codiedevCodexHookTrustEntries(hooksPath, hooksFile) {
128
+ const events = hooksFile?.hooks;
129
+ if (!events || typeof events !== "object")
130
+ return [];
131
+ const source = normalizedHooksPath(hooksPath);
132
+ const entries = [];
133
+ for (const [eventName, groupsValue] of Object.entries(events)) {
134
+ const eventLabel = EVENT_LABELS[eventName];
135
+ if (!eventLabel || !Array.isArray(groupsValue))
136
+ continue;
137
+ groupsValue.forEach((groupValue, groupIndex) => {
138
+ const group = groupValue;
139
+ if (!Array.isArray(group.hooks))
140
+ return;
141
+ group.hooks.forEach((handlerValue, handlerIndex) => {
142
+ const handler = handlerValue;
143
+ if (handler.type !== "command" || typeof handler.command !== "string")
144
+ return;
145
+ if (!(0, detection_1.isCodiedevHookCommand)(handler.command))
146
+ return;
147
+ entries.push({
148
+ key: `${source}:${eventLabel}:${groupIndex}:${handlerIndex}`,
149
+ eventName,
150
+ command: handler.command,
151
+ currentHash: codexCommandHookHash(eventName, group, handler),
152
+ });
153
+ });
154
+ });
155
+ }
156
+ return entries;
157
+ }
158
+ function tableHeader(key) {
159
+ return `[hooks.state.${JSON.stringify(key)}]`;
160
+ }
161
+ function removeTable(lines, header) {
162
+ const output = [];
163
+ let skipping = false;
164
+ for (const line of lines) {
165
+ if (line.trim() === header) {
166
+ skipping = true;
167
+ continue;
168
+ }
169
+ if (skipping && /^\s*\[/.test(line))
170
+ skipping = false;
171
+ if (!skipping)
172
+ output.push(line);
173
+ }
174
+ return output;
175
+ }
176
+ /** `codiedev connect` is an explicit install action, so trust only the exact
177
+ * Codiedev commands it just wrote. Any later command change produces a new
178
+ * hash and Codex disables that hook until connect is run again. */
179
+ function writeCodexHookTrust(configPath, hooksPath, hooksFile) {
180
+ const entries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
181
+ let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
182
+ existing = existing.replace(new RegExp(`\\n?${TRUST_BEGIN}\\n[\\s\\S]*?${TRUST_END}\\n?`, "g"), "\n");
183
+ let lines = existing.split("\n");
184
+ for (const entry of entries)
185
+ lines = removeTable(lines, tableHeader(entry.key));
186
+ const base = lines.join("\n").trimEnd();
187
+ const trustBlock = entries.flatMap((entry) => [
188
+ tableHeader(entry.key),
189
+ "enabled = true",
190
+ `trusted_hash = ${JSON.stringify(entry.currentHash)}`,
191
+ "",
192
+ ]).join("\n");
193
+ const next = `${base}${base ? "\n\n" : ""}${TRUST_BEGIN}\n${trustBlock}${TRUST_END}\n`;
194
+ fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
195
+ fs.writeFileSync(configPath, next, { encoding: "utf8", mode: 0o600 });
196
+ fs.chmodSync(configPath, 0o600);
197
+ return entries;
198
+ }
199
+ function configuredTrust(configText, key) {
200
+ const lines = configText.split("\n");
201
+ const header = tableHeader(key);
202
+ const start = lines.findIndex((line) => line.trim() === header);
203
+ if (start < 0)
204
+ return { enabled: true };
205
+ let enabled = true;
206
+ let hash;
207
+ for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index]); index += 1) {
208
+ const enabledMatch = lines[index].match(/^\s*enabled\s*=\s*(true|false)\s*$/);
209
+ if (enabledMatch)
210
+ enabled = enabledMatch[1] === "true";
211
+ const hashMatch = lines[index].match(/^\s*trusted_hash\s*=\s*"([^"]+)"\s*$/);
212
+ if (hashMatch)
213
+ hash = hashMatch[1];
214
+ }
215
+ return { enabled, hash };
216
+ }
217
+ function inspectCodexHookTrust(configPath, hooksPath, hooksFile, requiredSubcommands = []) {
218
+ const configText = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
219
+ const allEntries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
220
+ const entries = requiredSubcommands.length === 0
221
+ ? allEntries
222
+ : allEntries.filter((entry) => requiredSubcommands.some((subcommand) => entry.command.trim().endsWith(subcommand)));
223
+ const trusted = entries.filter((entry) => {
224
+ const state = configuredTrust(configText, entry.key);
225
+ return state.enabled && state.hash === entry.currentHash;
226
+ }).length;
227
+ return { ok: entries.length > 0 && trusted === entries.length, total: entries.length, trusted };
228
+ }
package/dist/codie.js CHANGED
@@ -232,9 +232,26 @@ async function main() {
232
232
  OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
233
233
  };
234
234
  const runtimeArgs = (0, codieLauncher_1.withCodieWorkingDirectory)(parsed.args, process.cwd());
235
- const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, { stdio: "inherit", env });
235
+ const nonInteractiveRun = runtimeArgs[0] === "run";
236
+ if (nonInteractiveRun)
237
+ env.CODIEDEV_SECURITY_BLOCK_OUTPUT = "marker";
238
+ const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, {
239
+ stdio: nonInteractiveRun ? ["inherit", "inherit", "pipe"] : "inherit",
240
+ encoding: nonInteractiveRun ? "utf8" : undefined,
241
+ env,
242
+ });
236
243
  if (result.error)
237
244
  throw result.error;
245
+ if (nonInteractiveRun) {
246
+ const stderr = typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() ?? "";
247
+ const blockReason = (0, codieLauncher_1.extractCodieSecurityBlock)(stderr);
248
+ if (blockReason) {
249
+ console.error(`Codie Security blocked this action:\n${blockReason}`);
250
+ }
251
+ else if (stderr) {
252
+ process.stderr.write(stderr);
253
+ }
254
+ }
238
255
  process.exitCode = result.status === null ? 1 : result.status;
239
256
  }
240
257
  main().catch((error) => {
@@ -2,6 +2,11 @@ export interface CatalogModel {
2
2
  id: string;
3
3
  name?: string;
4
4
  }
5
+ export declare const CODIE_SECURITY_BLOCK_MARKER = "__CODIEDEV_SECURITY_BLOCK__";
6
+ /** Extract the private adapter marker used to carry a local block reason
7
+ * across OpenCode's generic 500 boundary. The marker never contains prompt or
8
+ * tool input — only the deterministic policy reason. */
9
+ export declare function extractCodieSecurityBlock(stderr: string): string | undefined;
5
10
  /**
6
11
  * Pin non-interactive runs to the directory where `codie` was launched.
7
12
  * OpenCode can reuse a previously-known project instance; without an explicit
@@ -42,7 +42,8 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  };
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = void 0;
45
+ exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = exports.CODIE_SECURITY_BLOCK_MARKER = void 0;
46
+ exports.extractCodieSecurityBlock = extractCodieSecurityBlock;
46
47
  exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
47
48
  exports.bareModel = bareModel;
48
49
  exports.buildCodieConfig = buildCodieConfig;
@@ -62,6 +63,26 @@ const DEFAULT_NAMES = {
62
63
  "z-ai/glm-5.2": "GLM 5.2 · Fireworks ZDR",
63
64
  "moonshotai/kimi-k2.7-code": "Kimi K2.7 Code · Fireworks ZDR",
64
65
  };
66
+ exports.CODIE_SECURITY_BLOCK_MARKER = "__CODIEDEV_SECURITY_BLOCK__";
67
+ /** Extract the private adapter marker used to carry a local block reason
68
+ * across OpenCode's generic 500 boundary. The marker never contains prompt or
69
+ * tool input — only the deterministic policy reason. */
70
+ function extractCodieSecurityBlock(stderr) {
71
+ for (const line of stderr.split(/\r?\n/)) {
72
+ const markerIndex = line.indexOf(exports.CODIE_SECURITY_BLOCK_MARKER);
73
+ if (markerIndex < 0)
74
+ continue;
75
+ try {
76
+ const parsed = JSON.parse(line.slice(markerIndex + exports.CODIE_SECURITY_BLOCK_MARKER.length));
77
+ if (typeof parsed?.reason === "string" && parsed.reason.trim())
78
+ return parsed.reason.trim();
79
+ }
80
+ catch {
81
+ // Ignore malformed or foreign stderr and preserve the original runtime error.
82
+ }
83
+ }
84
+ return undefined;
85
+ }
65
86
  /**
66
87
  * Pin non-interactive runs to the directory where `codie` was launched.
67
88
  * OpenCode can reuse a previously-known project instance; without an explicit
package/dist/connect.js CHANGED
@@ -292,6 +292,7 @@ async function runConnect(args = []) {
292
292
  path.join(home, ".claude", "settings.json"),
293
293
  path.join(home, ".codex", "hooks.json"),
294
294
  path.join(home, ".config", "opencode", "plugins", "codiedev.js"),
295
+ path.join(home, ".codex", "config.toml"),
295
296
  ];
296
297
  // Snapshot before any hook update. Rollback is intentionally not automatic:
297
298
  // an administrator can inspect these 0600 copies before restoring them.
package/dist/hook.js CHANGED
@@ -269,7 +269,7 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
269
269
  // Local policy runs synchronously before a tool executes. Workflow telemetry
270
270
  // is dispatched separately and fail-open, so a detector outage or timeout can
271
271
  // never delay or override the deterministic endpoint decision.
272
- async function runSecurityHook(hookInput, phase, source) {
272
+ async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
273
273
  const config = (0, utils_1.readConfig)();
274
274
  if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
275
275
  return;
@@ -292,7 +292,8 @@ async function runSecurityHook(hookInput, phase, source) {
292
292
  outcome: "ok",
293
293
  detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
294
294
  });
295
- dispatchSecurityEventUpload(event);
295
+ if (uploadTelemetry)
296
+ dispatchSecurityEventUpload(event);
296
297
  process.stdout.write(JSON.stringify({
297
298
  hookSpecificOutput: {
298
299
  hookEventName: "PreToolUse",
@@ -304,16 +305,25 @@ async function runSecurityHook(hookInput, phase, source) {
304
305
  }
305
306
  if (!event)
306
307
  return;
307
- dispatchSecurityEventUpload(event);
308
+ if (uploadTelemetry)
309
+ dispatchSecurityEventUpload(event);
308
310
  }
309
- function runPromptSecurityHook(hookInput, source) {
311
+ function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
310
312
  const config = (0, utils_1.readConfig)();
311
313
  if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
312
314
  return false;
313
315
  const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
314
316
  if (decision.action !== "block")
315
317
  return false;
316
- dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
318
+ (0, captureLog_1.appendCaptureLog)({
319
+ stage: "security",
320
+ sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
321
+ outcome: "ok",
322
+ detail: `phase=prompt localDecision=block category=${decision.category} telemetry=${uploadTelemetry ? "enabled" : "suppressed"} cwd=${hookInput.cwd ?? "unknown"}`,
323
+ });
324
+ if (uploadTelemetry) {
325
+ dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
326
+ }
317
327
  process.stdout.write(JSON.stringify({
318
328
  decision: "block",
319
329
  reason: decision.reason,
@@ -566,13 +576,10 @@ async function main() {
566
576
  catch {
567
577
  process.exit(0);
568
578
  }
569
- // Local privacy boundary: every stdin-driven hook mode passes through this
570
- // check BEFORE prompt discovery, limit-event tails, SessionStart pings,
571
- // backup, self-heal, transcript reads, or detached dispatch. Ignored means
572
- // no Codiedev request at all for this session.
573
- if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
574
- process.exit(0);
575
- }
579
+ // Privacy exclusions suppress every Codiedev upload, but they must never
580
+ // disable local security enforcement. Security hooks run first with telemetry
581
+ // disabled; all other hook modes still exit before making a network request.
582
+ const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
576
583
  const securityCommand = process.argv[2] ?? "";
577
584
  const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|codie))?$/);
578
585
  if (securityMatch) {
@@ -581,7 +588,7 @@ async function main() {
581
588
  : securityMatch[2] === "codie"
582
589
  ? "codie-harness"
583
590
  : "claude-code";
584
- await runSecurityHook(hookInput, securityMatch[1], source);
591
+ await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
585
592
  process.exit(0);
586
593
  }
587
594
  // UserPromptSubmit discovery path — separate from session capture.
@@ -596,12 +603,16 @@ async function main() {
596
603
  : "claude-code";
597
604
  // Security runs first and locally. A blocked prompt is erased before it
598
605
  // reaches the model, so discovery must not send it to the backend.
599
- if (runPromptSecurityHook(hookInput, source))
606
+ if (runPromptSecurityHook(hookInput, source, !captureIgnored))
607
+ return;
608
+ if (captureIgnored)
600
609
  return;
601
610
  dispatchAllowedPromptSecurityEvent(hookInput, source);
602
611
  await runDiscover(hookInput.prompt);
603
612
  return;
604
613
  }
614
+ if (captureIgnored)
615
+ process.exit(0);
605
616
  // StopFailure (matcher rate_limit): post the rate-limit event.
606
617
  if (process.argv[2] === "limit") {
607
618
  await runStopFailure(hookInput);
@@ -57,5 +57,6 @@ export declare function restoreSecurityInstallSnapshot(manifest: SecurityInstall
57
57
  export declare function inspectSecurityInstallation(paths?: {
58
58
  claudeSettingsPath?: string;
59
59
  codexHooksPath?: string;
60
+ codexConfigPath?: string;
60
61
  codiePluginPath?: string;
61
62
  }): SecurityInstallInspection[];
@@ -45,6 +45,7 @@ const os = __importStar(require("os"));
45
45
  const path = __importStar(require("path"));
46
46
  const configDir_1 = require("./configDir");
47
47
  const detection_1 = require("./detection");
48
+ const codexHookTrust_1 = require("./codexHookTrust");
48
49
  const version_1 = require("./version");
49
50
  const MANIFEST_FILE = "security-install.json";
50
51
  function sha256File(filePath) {
@@ -169,7 +170,7 @@ function hookCommands(parsed, eventName) {
169
170
  }
170
171
  return commands;
171
172
  }
172
- function inspectJsonSurface(surface, filePath) {
173
+ function inspectJsonSurface(surface, filePath, codexConfigPath) {
173
174
  if (!fs.existsSync(filePath)) {
174
175
  return { surface, status: "not-installed", detail: `${filePath} not found` };
175
176
  }
@@ -180,14 +181,23 @@ function inspectJsonSurface(surface, filePath) {
180
181
  ["UserPromptSubmit", `discover${suffix}`],
181
182
  ["PreToolUse", `security-pre${suffix}`],
182
183
  ["PostToolUse", `security-post${suffix}`],
183
- ["PostToolUseFailure", `security-failure${suffix}`],
184
+ ...(surface === "claude-code"
185
+ ? [["PostToolUseFailure", `security-failure${suffix}`]]
186
+ : []),
184
187
  ];
185
188
  const missing = expected
186
189
  .filter(([eventName, subcommand]) => !hookCommands(parsed, eventName).some((command) => command.trim().endsWith(subcommand)))
187
190
  .map(([eventName]) => eventName);
188
- return missing.length === 0
189
- ? { surface, status: "pass", detail: "prompt guard and tool telemetry hooks installed" }
190
- : { surface, status: "fail", detail: `missing security hooks: ${missing.join(", ")}` };
191
+ if (missing.length > 0) {
192
+ return { surface, status: "fail", detail: `missing security hooks: ${missing.join(", ")}` };
193
+ }
194
+ if (surface === "codex") {
195
+ const trust = (0, codexHookTrust_1.inspectCodexHookTrust)(codexConfigPath ?? path.join(path.dirname(filePath), "config.toml"), filePath, parsed, ["discover-codex", "security-pre-codex", "security-post-codex"]);
196
+ if (!trust.ok) {
197
+ return { surface, status: "fail", detail: `Codex hook trust missing or stale (${trust.trusted}/${trust.total} trusted)` };
198
+ }
199
+ }
200
+ return { surface, status: "pass", detail: "prompt guard and tool telemetry hooks installed and active" };
191
201
  }
192
202
  catch (error) {
193
203
  return { surface, status: "fail", detail: `invalid hook configuration: ${error.message}` };
@@ -200,7 +210,7 @@ function inspectSecurityInstallation(paths = {}) {
200
210
  const codie = paths.codiePluginPath ?? path.join(home, ".config", "opencode", "plugins", "codiedev.js");
201
211
  const results = [
202
212
  inspectJsonSurface("claude-code", claude),
203
- inspectJsonSurface("codex", codex),
213
+ inspectJsonSurface("codex", codex, paths.codexConfigPath),
204
214
  ];
205
215
  if (!fs.existsSync(codie)) {
206
216
  results.push({ surface: "codie-harness", status: "not-installed", detail: `${codie} not found` });
package/dist/utils.js CHANGED
@@ -117,6 +117,7 @@ const detection_1 = require("./detection");
117
117
  const captureLog_1 = require("./captureLog");
118
118
  const configDir_1 = require("./configDir");
119
119
  Object.defineProperty(exports, "codiedevStateDir", { enumerable: true, get: function () { return configDir_1.codiedevStateDir; } });
120
+ const codexHookTrust_1 = require("./codexHookTrust");
120
121
  const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
121
122
  exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
122
123
  function readConfig() {
@@ -1076,6 +1077,7 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1076
1077
  `const AGENT_SECURITY_ENABLED = ${JSON.stringify(agentSecurityEnabled)};\n` +
1077
1078
  `const fingerprints = new Map();\n\n` +
1078
1079
  `const transcriptPaths = new Map();\n\n` +
1080
+ `const sessionDirectories = new Map();\n\n` +
1079
1081
  `function sessionId(event) {\n` +
1080
1082
  ` return event?.properties?.sessionID ?? event?.properties?.sessionId ?? event?.properties?.info?.id ?? null;\n` +
1081
1083
  `}\n\n` +
@@ -1091,20 +1093,44 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1091
1093
  `}\n\n` +
1092
1094
  `function enforceSync(invocation, payload) {\n` +
1093
1095
  ` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
1094
- ` if (result.error) return;\n` +
1096
+ ` if (result.error) return null;\n` +
1095
1097
  ` try {\n` +
1096
1098
  ` const decision = JSON.parse(result.stdout || "{}");\n` +
1097
1099
  ` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
1098
- ` if (denied) throw new Error(decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action");\n` +
1100
+ ` return denied ? (decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action") : null;\n` +
1099
1101
  ` } catch (error) {\n` +
1100
- ` if (error instanceof SyntaxError) return;\n` +
1101
- ` throw error;\n` +
1102
+ ` if (error instanceof SyntaxError) return null;\n` +
1103
+ ` return null;\n` +
1102
1104
  ` }\n` +
1103
1105
  `}\n\n` +
1104
1106
  `export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
1107
+ ` const enforce = async (invocation, payload) => {\n` +
1108
+ ` const reason = enforceSync(invocation, payload);\n` +
1109
+ ` if (!reason) return;\n` +
1110
+ ` try { await client.tui.showToast({ title: "Codie Security", message: reason, variant: "error", duration: 10000 }); } catch {}\n` +
1111
+ ` if (process.env.CODIEDEV_SECURITY_BLOCK_OUTPUT === "marker") {\n` +
1112
+ ` process.stderr.write("__CODIEDEV_SECURITY_BLOCK__" + JSON.stringify({ reason }) + "\\n");\n` +
1113
+ ` } else {\n` +
1114
+ ` process.stderr.write("\\nCodie Security blocked this action:\\n" + reason + "\\n");\n` +
1115
+ ` }\n` +
1116
+ ` throw new Error(reason);\n` +
1117
+ ` };\n` +
1118
+ ` const resolveCwd = async (id, hint) => {\n` +
1119
+ ` if (typeof hint === "string" && hint && hint !== "/") { sessionDirectories.set(id, hint); return hint; }\n` +
1120
+ ` const remembered = sessionDirectories.get(id);\n` +
1121
+ ` if (remembered) return remembered;\n` +
1122
+ ` try {\n` +
1123
+ ` const response = await client.session.get({ path: { id } });\n` +
1124
+ ` const session = response?.data ?? response;\n` +
1125
+ ` if (typeof session?.directory === "string" && session.directory) { sessionDirectories.set(id, session.directory); return session.directory; }\n` +
1126
+ ` } catch {}\n` +
1127
+ ` const fallback = worktree || directory || process.cwd();\n` +
1128
+ ` sessionDirectories.set(id, fallback);\n` +
1129
+ ` return fallback;\n` +
1130
+ ` };\n` +
1105
1131
  ` const finalizeAll = () => {\n` +
1106
1132
  ` for (const [id, transcriptPath] of transcriptPaths) {\n` +
1107
- ` const cwd = worktree || directory || process.cwd();\n` +
1133
+ ` const cwd = sessionDirectories.get(id) || worktree || directory || process.cwd();\n` +
1108
1134
  ` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1109
1135
  ` }\n` +
1110
1136
  ` };\n` +
@@ -1112,25 +1138,27 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1112
1138
  ` return ({\n` +
1113
1139
  ` "chat.message": async (input, output) => {\n` +
1114
1140
  ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1115
- ` const cwd = worktree || directory || process.cwd();\n` +
1141
+ ` const cwd = await resolveCwd(id);\n` +
1116
1142
  ` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
1117
- ` enforceSync(PROMPT, { sessionId: id, cwd, prompt });\n` +
1143
+ ` await enforce(PROMPT, { sessionId: id, cwd, prompt });\n` +
1118
1144
  ` },\n` +
1119
1145
  ` "tool.execute.before": async (input, output) => {\n` +
1120
1146
  ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1121
- ` const cwd = worktree || directory || process.cwd();\n` +
1122
- ` enforceSync(SECURITY_PRE, { sessionId: input?.sessionID ?? input?.sessionId ?? "unknown", cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
1147
+ ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1148
+ ` const cwd = await resolveCwd(id);\n` +
1149
+ ` await enforce(SECURITY_PRE, { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
1123
1150
  ` },\n` +
1124
1151
  ` "tool.execute.after": async (input, output) => {\n` +
1125
1152
  ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1126
- ` const cwd = worktree || directory || process.cwd();\n` +
1127
- ` const payload = { sessionId: input?.sessionID ?? input?.sessionId ?? "unknown", cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
1153
+ ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1154
+ ` const cwd = await resolveCwd(id);\n` +
1155
+ ` const payload = { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
1128
1156
  ` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
1129
1157
  ` },\n` +
1130
1158
  ` event: async ({ event }) => {\n` +
1131
1159
  ` const id = sessionId(event);\n` +
1132
1160
  ` if (!id) return;\n` +
1133
- ` const cwd = worktree || directory || process.cwd();\n` +
1161
+ ` const cwd = await resolveCwd(id, event?.properties?.info?.directory);\n` +
1134
1162
  ` if (event.type === "session.created") {\n` +
1135
1163
  ` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
1136
1164
  ` return;\n` +
@@ -1139,6 +1167,7 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1139
1167
  ` const transcriptPath = transcriptPaths.get(id);\n` +
1140
1168
  ` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1141
1169
  ` transcriptPaths.delete(id);\n` +
1170
+ ` sessionDirectories.delete(id);\n` +
1142
1171
  ` fingerprints.delete(id);\n` +
1143
1172
  ` return;\n` +
1144
1173
  ` }\n` +
@@ -1908,6 +1937,7 @@ function installCodexHook(agentSecurityEnabled = true) {
1908
1937
  fs.mkdirSync(CODEX_DIR, { recursive: true });
1909
1938
  }
1910
1939
  fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1940
+ (0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
1911
1941
  }
1912
1942
  // Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
1913
1943
  // `sessionEnd` fires when a conversation closes (completed, aborted, error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.39",
3
+ "version": "0.9.41",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",