codiedev 0.9.39 → 0.9.40

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/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` +
@@ -1102,9 +1104,22 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1102
1104
  ` }\n` +
1103
1105
  `}\n\n` +
1104
1106
  `export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
1107
+ ` const resolveCwd = async (id, hint) => {\n` +
1108
+ ` if (typeof hint === "string" && hint && hint !== "/") { sessionDirectories.set(id, hint); return hint; }\n` +
1109
+ ` const remembered = sessionDirectories.get(id);\n` +
1110
+ ` if (remembered) return remembered;\n` +
1111
+ ` try {\n` +
1112
+ ` const response = await client.session.get({ path: { id } });\n` +
1113
+ ` const session = response?.data ?? response;\n` +
1114
+ ` if (typeof session?.directory === "string" && session.directory) { sessionDirectories.set(id, session.directory); return session.directory; }\n` +
1115
+ ` } catch {}\n` +
1116
+ ` const fallback = worktree || directory || process.cwd();\n` +
1117
+ ` sessionDirectories.set(id, fallback);\n` +
1118
+ ` return fallback;\n` +
1119
+ ` };\n` +
1105
1120
  ` const finalizeAll = () => {\n` +
1106
1121
  ` for (const [id, transcriptPath] of transcriptPaths) {\n` +
1107
- ` const cwd = worktree || directory || process.cwd();\n` +
1122
+ ` const cwd = sessionDirectories.get(id) || worktree || directory || process.cwd();\n` +
1108
1123
  ` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1109
1124
  ` }\n` +
1110
1125
  ` };\n` +
@@ -1112,25 +1127,27 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1112
1127
  ` return ({\n` +
1113
1128
  ` "chat.message": async (input, output) => {\n` +
1114
1129
  ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1115
- ` const cwd = worktree || directory || process.cwd();\n` +
1130
+ ` const cwd = await resolveCwd(id);\n` +
1116
1131
  ` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
1117
1132
  ` enforceSync(PROMPT, { sessionId: id, cwd, prompt });\n` +
1118
1133
  ` },\n` +
1119
1134
  ` "tool.execute.before": async (input, output) => {\n` +
1120
1135
  ` 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` +
1136
+ ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1137
+ ` const cwd = await resolveCwd(id);\n` +
1138
+ ` enforceSync(SECURITY_PRE, { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
1123
1139
  ` },\n` +
1124
1140
  ` "tool.execute.after": async (input, output) => {\n` +
1125
1141
  ` 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` +
1142
+ ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1143
+ ` const cwd = await resolveCwd(id);\n` +
1144
+ ` const payload = { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
1128
1145
  ` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
1129
1146
  ` },\n` +
1130
1147
  ` event: async ({ event }) => {\n` +
1131
1148
  ` const id = sessionId(event);\n` +
1132
1149
  ` if (!id) return;\n` +
1133
- ` const cwd = worktree || directory || process.cwd();\n` +
1150
+ ` const cwd = await resolveCwd(id, event?.properties?.info?.directory);\n` +
1134
1151
  ` if (event.type === "session.created") {\n` +
1135
1152
  ` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
1136
1153
  ` return;\n` +
@@ -1139,6 +1156,7 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1139
1156
  ` const transcriptPath = transcriptPaths.get(id);\n` +
1140
1157
  ` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1141
1158
  ` transcriptPaths.delete(id);\n` +
1159
+ ` sessionDirectories.delete(id);\n` +
1142
1160
  ` fingerprints.delete(id);\n` +
1143
1161
  ` return;\n` +
1144
1162
  ` }\n` +
@@ -1908,6 +1926,7 @@ function installCodexHook(agentSecurityEnabled = true) {
1908
1926
  fs.mkdirSync(CODEX_DIR, { recursive: true });
1909
1927
  }
1910
1928
  fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1929
+ (0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
1911
1930
  }
1912
1931
  // Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
1913
1932
  // `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.40",
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",