fullcourtdefense-cli 1.34.0 → 1.34.1

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,19 @@
1
+ export declare function resolveCodexExecutable(localAppData?: string): string;
2
+ export interface CodexHook {
3
+ eventName: string;
4
+ command?: string;
5
+ enabled: boolean;
6
+ isManaged: boolean;
7
+ trustStatus: string;
8
+ sourcePath: string;
9
+ }
10
+ export interface CodexHookStatus {
11
+ state: 'unavailable' | 'not_configured' | 'approval_required' | 'disabled' | 'configured_unverified' | 'ready_for_probe';
12
+ managed: boolean;
13
+ message: string;
14
+ }
15
+ export declare function isFcdCodexHook(command: unknown): boolean;
16
+ /** Configuration readiness is deliberately not a claim that the backend received an event. */
17
+ export declare function summarizeCodexHooks(hooks: CodexHook[]): CodexHookStatus;
18
+ /** Read-only app-server handshake. No thread, model turn, trust write, or hook execution. */
19
+ export declare function getCodexHookStatus(cwd?: string, executable?: string, timeoutMs?: number): Promise<CodexHookStatus>;
@@ -0,0 +1,156 @@
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.resolveCodexExecutable = resolveCodexExecutable;
37
+ exports.isFcdCodexHook = isFcdCodexHook;
38
+ exports.summarizeCodexHooks = summarizeCodexHooks;
39
+ exports.getCodexHookStatus = getCodexHookStatus;
40
+ const child_process_1 = require("child_process");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ function resolveCodexExecutable(localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')) {
45
+ if (process.platform !== 'win32')
46
+ return 'codex';
47
+ const bin = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
48
+ try {
49
+ const candidates = fs.readdirSync(bin).map(version => path.join(bin, version, 'codex.exe'))
50
+ .filter(file => fs.existsSync(file) && fs.statSync(file).isFile());
51
+ candidates.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
52
+ if (candidates.length)
53
+ return candidates[0];
54
+ }
55
+ catch { /* CLI-only or desktop absent: let spawn resolve PATH. */ }
56
+ return 'codex';
57
+ }
58
+ const EVENTS = ['preToolUse', 'postToolUse', 'userPromptSubmit'];
59
+ function isFcdCodexHook(command) {
60
+ if (typeof command !== 'string')
61
+ return false;
62
+ const encoded = /-EncodedCommand ([A-Za-z0-9+/=]+)$/.exec(command)?.[1];
63
+ const decoded = encoded ? Buffer.from(encoded, 'base64').toString('utf16le') : command;
64
+ return decoded.includes('--fcd-native-managed true') && decoded.includes('--agent-client codex');
65
+ }
66
+ /** Configuration readiness is deliberately not a claim that the backend received an event. */
67
+ function summarizeCodexHooks(hooks) {
68
+ const ours = hooks.filter(hook => isFcdCodexHook(hook.command));
69
+ const usable = (h) => h.enabled && (h.isManaged || h.trustStatus === 'trusted');
70
+ const complete = (items) => EVENTS.every(event => items.some(h => h.eventName === event));
71
+ const managed = complete(ours.filter(h => usable(h) && h.isManaged));
72
+ if (complete(ours.filter(usable)))
73
+ return { state: 'ready_for_probe', managed,
74
+ message: 'Codex hooks are trusted and enabled. Capture is not yet verified: run an action in the Codex app and confirm its event in Activity.' };
75
+ if (!complete(ours))
76
+ return { state: 'not_configured', managed: false,
77
+ message: 'Codex capture is incomplete: one or more FullCourtDefense hooks are missing.' };
78
+ if (ours.some(h => h.enabled && !h.isManaged && h.trustStatus !== 'trusted'))
79
+ return {
80
+ state: 'approval_required', managed: false,
81
+ message: 'Codex capture is inactive: hook approval is required. Company devices should use the administrator-managed installation.'
82
+ };
83
+ return { state: 'disabled', managed: false, message: 'Codex capture is inactive: required hooks are disabled.' };
84
+ }
85
+ /** Read-only app-server handshake. No thread, model turn, trust write, or hook execution. */
86
+ function getCodexHookStatus(cwd = process.cwd(), executable = resolveCodexExecutable(), timeoutMs = 10_000) {
87
+ return new Promise(resolve => {
88
+ const child = (0, child_process_1.spawn)(executable, ['app-server', '--stdio'], { stdio: ['pipe', 'pipe', 'ignore'], windowsHide: true });
89
+ let finished = false;
90
+ let buffer = '';
91
+ let received = 0;
92
+ const finish = (status) => {
93
+ if (finished)
94
+ return;
95
+ finished = true;
96
+ clearTimeout(timer);
97
+ child.kill();
98
+ resolve(status);
99
+ };
100
+ const unavailable = (message) => finish({ state: 'unavailable', managed: false, message });
101
+ const timer = setTimeout(() => unavailable('Codex hook status timed out; capture is not verified.'), timeoutMs);
102
+ const send = (value) => child.stdin.write(JSON.stringify(value) + '\n');
103
+ child.on('error', () => unavailable('Codex could not be started for hook diagnostics; capture is not verified.'));
104
+ child.stdin.on('error', () => unavailable('Codex hook diagnostics disconnected; capture is not verified.'));
105
+ child.on('exit', () => unavailable('Codex exited before reporting hook status; capture is not verified.'));
106
+ child.stdout.setEncoding('utf8');
107
+ child.stdout.on('data', (chunk) => {
108
+ received += Buffer.byteLength(chunk);
109
+ if (received > 2_000_000)
110
+ return unavailable('Codex hook diagnostics exceeded the response limit.');
111
+ buffer += chunk;
112
+ let newline;
113
+ while (!finished && (newline = buffer.indexOf('\n')) >= 0) {
114
+ const line = buffer.slice(0, newline);
115
+ buffer = buffer.slice(newline + 1);
116
+ let reply;
117
+ try {
118
+ reply = JSON.parse(line);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ if (![1, 2, 3].includes(reply.id))
124
+ continue;
125
+ if (reply.error)
126
+ return unavailable('Codex rejected hook diagnostics; update or repair the Codex installation.');
127
+ if (reply.id === 1) {
128
+ send({ method: 'initialized' });
129
+ send({ method: 'config/read', id: 3, params: { cwd, includeLayers: false } });
130
+ }
131
+ else if (reply.id === 3) {
132
+ const features = reply.result?.config?.features;
133
+ if (features?.hooks === false || (features?.hooks === undefined && features?.codex_hooks === false)) {
134
+ return finish({ state: 'disabled', managed: false, message: 'Codex capture is inactive: hooks are disabled in the effective Codex configuration.' });
135
+ }
136
+ if (!reply.result?.config)
137
+ return unavailable('Codex did not report its effective configuration; capture is not verified.');
138
+ send({ method: 'hooks/list', id: 2, params: { cwds: [cwd] } });
139
+ }
140
+ else {
141
+ const entry = reply.result?.data?.[0];
142
+ if (!entry || !Array.isArray(entry.hooks) || entry.errors?.length)
143
+ return unavailable('Codex reported invalid hook configuration; capture is not verified.');
144
+ if (!entry.hooks.every((h) => h && typeof h.eventName === 'string' && typeof h.enabled === 'boolean'
145
+ && typeof h.isManaged === 'boolean' && typeof h.trustStatus === 'string')) {
146
+ return unavailable('Codex returned an unsupported hook status format; capture is not verified.');
147
+ }
148
+ finish(summarizeCodexHooks(entry.hooks));
149
+ }
150
+ }
151
+ });
152
+ send({ method: 'initialize', id: 1, params: {
153
+ clientInfo: { name: 'fullcourtdefense_hook_status', version: '1.0' }, capabilities: { experimentalApi: true },
154
+ } });
155
+ });
156
+ }
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.installAllCommand = installAllCommand;
4
37
  const config_1 = require("../config");
@@ -6,6 +39,10 @@ const discover_1 = require("./discover");
6
39
  const installCursorHook_1 = require("./installCursorHook");
7
40
  const installClaudeHook_1 = require("./installClaudeHook");
8
41
  const installNativeHooks_1 = require("./installNativeHooks");
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const installCodexManagedHooks_1 = require("./installCodexManagedHooks");
9
46
  const discoverSchedule_1 = require("./discoverSchedule");
10
47
  const mcpGateway_1 = require("./mcpGateway");
11
48
  const autoProtect_1 = require("./autoProtect");
@@ -40,11 +77,18 @@ async function installAllCommand(args, config) {
40
77
  const nativeFiles = (0, installNativeHooks_1.installNativeHooks)();
41
78
  for (const file of nativeFiles)
42
79
  console.log(`Native action hooks configured: ${file}`);
43
- if (nativeFiles.some(file => file.includes('.codex')))
44
- console.log('Codex: review and trust the new hooks using /hooks before they can run.');
80
+ if (fs.existsSync(path.join(os.homedir(), '.codex'))) {
81
+ const status = await (0, installCodexManagedHooks_1.configureCodexForInstall)();
82
+ console.log(status.message);
83
+ if (status.state !== 'ready_for_probe')
84
+ console.warn('Codex activation remains incomplete; other client setup can continue.');
85
+ else if (status.managed)
86
+ (0, installNativeHooks_1.installNativeHooks)({ clients: ['codex'], remove: true });
87
+ }
45
88
  }
46
89
  catch (error) {
47
90
  console.error(`Native hooks incomplete: ${error instanceof Error ? error.message : String(error)}. Repair the settings and rerun install-native-hooks.`);
91
+ process.exitCode = 1;
48
92
  }
49
93
  console.log('\n\x1b[1mInstalling Cursor runtime hooks…\x1b[0m');
50
94
  const hookArgs = {
@@ -194,6 +238,7 @@ async function installAllCommand(args, config) {
194
238
  console.log('You can retry with: fullcourtdefense discover --schedule daily');
195
239
  }
196
240
  }
197
- console.log('\n\x1b[32mDone.\x1b[0m Run \x1b[1mfullcourtdefense protect-all --dry-run true\x1b[0m to verify gateways.');
241
+ console.log(process.exitCode ? '\nSetup is incomplete; resolve the activation problem reported above.'
242
+ : '\nSetup configuration finished. Verify a fresh action from each IDE in Activity before treating its capture as active.');
198
243
  await (0, restartNotice_1.promptRestartNotice)();
199
244
  }
@@ -0,0 +1,17 @@
1
+ export { isWindowsAdministrator } from '../windowsManagedAccess';
2
+ import { CodexHookStatus } from '../codexHookStatus';
3
+ export declare function codexSystemHookFile(): string;
4
+ /** Install only from the MSI's stable, administrator-controlled runtime. */
5
+ export declare function installedCodexRuntime(): {
6
+ executable: string;
7
+ entry: string;
8
+ };
9
+ /** npm-only administrators do not have an MSI runtime to deploy as managed code. */
10
+ export declare function configureCodexForInstall(): Promise<CodexHookStatus>;
11
+ /** Caller has checked administrator access and protected paths. Injectable read-only verification for tests. */
12
+ export declare function writeVerifiedCodexHooks(file: string, command: string, verify?: () => Promise<CodexHookStatus>): Promise<CodexHookStatus>;
13
+ export declare function codexTransactionJournal(directory: string, transaction: string): string;
14
+ export declare function installCodexManagedHooks(transaction?: string): Promise<CodexHookStatus>;
15
+ /** Fixed protected paths only; invoked by MSI rollback/commit in SYSTEM context. */
16
+ export declare function finishCodexManagedTransaction(rollback: boolean, transaction: string): void;
17
+ export declare function uninstallCodexManagedHooks(): void;
@@ -0,0 +1,166 @@
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.isWindowsAdministrator = void 0;
37
+ exports.codexSystemHookFile = codexSystemHookFile;
38
+ exports.installedCodexRuntime = installedCodexRuntime;
39
+ exports.configureCodexForInstall = configureCodexForInstall;
40
+ exports.writeVerifiedCodexHooks = writeVerifiedCodexHooks;
41
+ exports.codexTransactionJournal = codexTransactionJournal;
42
+ exports.installCodexManagedHooks = installCodexManagedHooks;
43
+ exports.finishCodexManagedTransaction = finishCodexManagedTransaction;
44
+ exports.uninstallCodexManagedHooks = uninstallCodexManagedHooks;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const windowsManagedAccess_1 = require("../windowsManagedAccess");
48
+ var windowsManagedAccess_2 = require("../windowsManagedAccess");
49
+ Object.defineProperty(exports, "isWindowsAdministrator", { enumerable: true, get: function () { return windowsManagedAccess_2.isWindowsAdministrator; } });
50
+ const crypto_1 = require("crypto");
51
+ const installNativeHooks_1 = require("./installNativeHooks");
52
+ const codexHookStatus_1 = require("../codexHookStatus");
53
+ function codexSystemHookFile() {
54
+ return path.join(process.env.ProgramData || 'C:\\ProgramData', 'OpenAI', 'Codex', 'hooks.json');
55
+ }
56
+ /** Install only from the MSI's stable, administrator-controlled runtime. */
57
+ function installedCodexRuntime() {
58
+ const root = path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'FullCourtDefense');
59
+ const runtime = { executable: path.join(root, 'runtime', 'node.exe'), entry: path.join(root, 'dist', 'nativeHook.js') };
60
+ for (const item of [root, path.dirname(runtime.entry), path.dirname(runtime.executable), runtime.executable, runtime.entry]) {
61
+ if (!fs.existsSync(item) || fs.lstatSync(item).isSymbolicLink())
62
+ throw new Error('Install or repair the FullCourtDefense MSI before activating managed Codex capture.');
63
+ }
64
+ return runtime;
65
+ }
66
+ /** npm-only administrators do not have an MSI runtime to deploy as managed code. */
67
+ async function configureCodexForInstall() {
68
+ if (!(0, windowsManagedAccess_1.isWindowsAdministrator)())
69
+ return (0, codexHookStatus_1.getCodexHookStatus)();
70
+ const root = path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'FullCourtDefense');
71
+ if (!fs.existsSync(root))
72
+ return { state: 'configured_unverified', managed: false,
73
+ message: 'Codex user hooks are configured. Check hook trust from your normal Codex account; machine-managed deployment requires the MSI.' };
74
+ return installCodexManagedHooks();
75
+ }
76
+ /** Caller has checked administrator access and protected paths. Injectable read-only verification for tests. */
77
+ async function writeVerifiedCodexHooks(file, command, verify = codexHookStatus_1.getCodexHookStatus) {
78
+ const before = fs.existsSync(file) ? fs.readFileSync(file) : undefined;
79
+ (0, installNativeHooks_1.installNativeHooks)({ clients: ['codex'], codexSystem: { file, command } });
80
+ try {
81
+ const status = await verify();
82
+ if (status.state !== 'ready_for_probe' || !status.managed)
83
+ throw new Error(status.message);
84
+ return status;
85
+ }
86
+ catch (error) {
87
+ if (before)
88
+ fs.writeFileSync(file, before);
89
+ else
90
+ fs.unlinkSync(file);
91
+ throw new Error(`Managed Codex activation could not be verified; previous configuration restored. ${error instanceof Error ? error.message : String(error)}`);
92
+ }
93
+ }
94
+ function codexTransactionJournal(directory, transaction) {
95
+ if (!transaction || transaction.length > 256)
96
+ throw new Error('An MSI transaction identifier is required.');
97
+ return path.join(directory, `fullcourtdefense-rollback-${(0, crypto_1.createHash)('sha256').update(transaction).digest('hex')}.json`);
98
+ }
99
+ async function installCodexManagedHooks(transaction) {
100
+ if (!(0, windowsManagedAccess_1.isWindowsAdministrator)())
101
+ throw new Error('Managed Codex capture requires administrator deployment. Run setup elevated through your IT deployment tool. No employee /hooks approval is required for managed hooks.');
102
+ const runtime = installedCodexRuntime();
103
+ const file = codexSystemHookFile();
104
+ const directory = path.dirname(file);
105
+ for (const ancestor of [path.dirname(directory), directory]) {
106
+ if (fs.existsSync(ancestor) && fs.lstatSync(ancestor).isSymbolicLink())
107
+ throw new Error('Managed capture refuses redirected configuration paths.');
108
+ }
109
+ // Never promote a user-writable directory into a trusted execution source.
110
+ if (!fs.existsSync(directory)) {
111
+ fs.mkdirSync(directory, { recursive: true });
112
+ (0, windowsManagedAccess_1.protectNewManagedDirectory)(directory);
113
+ }
114
+ const configFile = path.join(directory, 'config.toml');
115
+ for (const item of [path.dirname(directory), directory, ...[file, configFile].filter(p => fs.existsSync(p)),
116
+ path.dirname(path.dirname(runtime.entry)), path.dirname(runtime.entry), path.dirname(runtime.executable), runtime.executable, runtime.entry]) {
117
+ if (fs.lstatSync(item).isSymbolicLink())
118
+ throw new Error('Managed capture refuses redirected configuration or runtime paths.');
119
+ (0, windowsManagedAccess_1.verifyManagedPath)(item, item === path.dirname(directory));
120
+ }
121
+ // SYSTEM must never launch a user's Codex binary or alter the SYSTEM profile's
122
+ // user hooks. The impersonated onboarding step verifies the user's client.
123
+ if (transaction) {
124
+ const snapshot = [file, configFile].map(p => fs.existsSync(p) ? fs.readFileSync(p).toString('base64') : null);
125
+ fs.writeFileSync(codexTransactionJournal(directory, transaction), JSON.stringify(snapshot), { flag: 'wx' });
126
+ }
127
+ (0, installNativeHooks_1.installNativeHooks)({ clients: ['codex'], codexSystem: { file, command: (0, installNativeHooks_1.nativeHookCommand)('codex', runtime) } });
128
+ if (!fs.existsSync(configFile))
129
+ fs.writeFileSync(configFile, '# FullCourtDefense: activate the system hook configuration layer.\n', { flag: 'wx' });
130
+ return { state: 'configured_unverified', managed: false,
131
+ message: 'Managed Codex hooks installed. The customer Codex client must verify activation; Activity capture is not yet verified.' };
132
+ }
133
+ /** Fixed protected paths only; invoked by MSI rollback/commit in SYSTEM context. */
134
+ function finishCodexManagedTransaction(rollback, transaction) {
135
+ if (!(0, windowsManagedAccess_1.isWindowsAdministrator)())
136
+ throw new Error('Administrator deployment is required.');
137
+ const directory = path.dirname(codexSystemHookFile());
138
+ const journal = codexTransactionJournal(directory, transaction);
139
+ if (!fs.existsSync(journal))
140
+ return;
141
+ if (fs.lstatSync(directory).isSymbolicLink() || fs.lstatSync(journal).isSymbolicLink())
142
+ throw new Error('Refusing redirected rollback configuration.');
143
+ if (rollback) {
144
+ const snapshot = JSON.parse(fs.readFileSync(journal, 'utf8'));
145
+ if (!Array.isArray(snapshot) || snapshot.length !== 2 || !snapshot.every(s => s === null || typeof s === 'string'))
146
+ throw new Error('Invalid managed hook rollback snapshot.');
147
+ [codexSystemHookFile(), path.join(directory, 'config.toml')].forEach((file, i) => {
148
+ if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink())
149
+ throw new Error('Refusing redirected rollback target.');
150
+ if (snapshot[i] === null) {
151
+ if (fs.existsSync(file))
152
+ fs.unlinkSync(file);
153
+ }
154
+ else
155
+ fs.writeFileSync(file, Buffer.from(snapshot[i], 'base64'));
156
+ });
157
+ }
158
+ fs.unlinkSync(journal);
159
+ }
160
+ function uninstallCodexManagedHooks() {
161
+ if (process.platform !== 'win32' || !fs.existsSync(codexSystemHookFile()))
162
+ return;
163
+ if (!(0, windowsManagedAccess_1.isWindowsAdministrator)())
164
+ throw new Error('Administrator access is required to remove machine-managed Codex hooks.');
165
+ (0, installNativeHooks_1.installNativeHooks)({ clients: ['codex'], remove: true, codexSystem: { file: codexSystemHookFile(), command: '' } });
166
+ }
@@ -1,9 +1,17 @@
1
1
  import type { NativeClient } from '../nativeHook';
2
- export declare function nativeHookCommand(client: NativeClient): string;
2
+ export declare function nativeHookCommand(client: NativeClient, runtime?: {
3
+ executable: string;
4
+ entry: string;
5
+ }): string;
3
6
  /** Preserve unrelated settings and hooks; refuse malformed files. */
4
7
  export declare function installNativeHooks(options?: {
5
8
  home?: string;
6
9
  project?: string;
7
10
  clients?: NativeClient[];
8
11
  remove?: boolean;
12
+ /** Internal target supplied by the administrator installer; never a CLI path override. */
13
+ codexSystem?: {
14
+ file: string;
15
+ command: string;
16
+ };
9
17
  }): string[];
@@ -48,18 +48,19 @@ const managed = (value) => {
48
48
  return Boolean(encoded && Buffer.from(encoded, 'base64').toString('utf16le').includes(marker));
49
49
  };
50
50
  const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
51
- function nativeHookCommand(client) {
51
+ function nativeHookCommand(client, runtime) {
52
52
  if (!['codex', 'gemini_cli', 'windsurf', 'kiro'].includes(client))
53
53
  throw new Error('Unsupported native hook client');
54
- const entry = path.resolve(__dirname, '..', 'nativeHook.js');
54
+ const entry = runtime?.entry || path.resolve(__dirname, '..', 'nativeHook.js');
55
+ const executable = runtime?.executable || process.execPath;
55
56
  const flags = `--agent-client ${client} ${marker} --fcd-managed true --local-only true`;
56
57
  if (process.platform !== 'win32')
57
- return `${quote(process.execPath)} ${quote(entry)} ${flags}`;
58
+ return `${quote(executable)} ${quote(entry)} ${flags}`;
58
59
  // Windows vendors use different shells. An encoded PowerShell launcher is
59
60
  // valid in both cmd and PowerShell, preserves paths with spaces, and forwards
60
61
  // the JSON stdin explicitly. Paths are data, never interpolated expressions.
61
62
  const psQuote = (value) => `'${value.replace(/'/g, "''")}'`;
62
- const script = `$ProgressPreference='SilentlyContinue'; $OutputEncoding=[Text.UTF8Encoding]::new($false); [Console]::InputEncoding=$OutputEncoding; [Console]::OutputEncoding=$OutputEncoding; $raw=[Console]::In.ReadToEnd(); $raw | & ${psQuote(process.execPath)} ${psQuote(entry)} ${flags}; exit $LASTEXITCODE`;
63
+ const script = `$ProgressPreference='SilentlyContinue'; $OutputEncoding=[Text.UTF8Encoding]::new($false); [Console]::InputEncoding=$OutputEncoding; [Console]::OutputEncoding=$OutputEncoding; $raw=[Console]::In.ReadToEnd(); $raw | & ${psQuote(executable)} ${psQuote(entry)} ${flags}; exit $LASTEXITCODE`;
63
64
  return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, 'utf16le').toString('base64')}`;
64
65
  }
65
66
  /** Preserve unrelated settings and hooks; refuse malformed files. */
@@ -76,7 +77,8 @@ function installNativeHooks(options = {}) {
76
77
  // Install only detected clients. Explicit client selection also supports setup before first launch.
77
78
  if (!options.clients && !fs.existsSync(root))
78
79
  continue;
79
- const file = path.join(root, client === 'kiro' ? 'hooks/fullcourtdefense.json' : client === 'gemini_cli' ? 'settings.json' : 'hooks.json');
80
+ const file = client === 'codex' && options.codexSystem ? options.codexSystem.file
81
+ : path.join(root, client === 'kiro' ? 'hooks/fullcourtdefense.json' : client === 'gemini_cli' ? 'settings.json' : 'hooks.json');
80
82
  if (options.remove && !fs.existsSync(file))
81
83
  continue;
82
84
  try {
@@ -84,7 +86,7 @@ function installNativeHooks(options = {}) {
84
86
  if (!data || typeof data !== 'object' || Array.isArray(data))
85
87
  throw new Error(`Invalid hook settings: ${file}`);
86
88
  const before = JSON.stringify(data);
87
- const command = nativeHookCommand(client);
89
+ const command = client === 'codex' && options.codexSystem ? options.codexSystem.command : nativeHookCommand(client);
88
90
  if (client === 'kiro') {
89
91
  if (data.hooks !== undefined && !Array.isArray(data.hooks))
90
92
  throw new Error(`Invalid Kiro hooks: ${file}`);
@@ -0,0 +1,3 @@
1
+ import { CodexHookStatus } from '../codexHookStatus';
2
+ /** Run as the customer at daemon startup/repair, including silent MSI upgrades. */
3
+ export declare function migrateCodexUserHooks(home?: string, verify?: () => Promise<CodexHookStatus>): Promise<boolean>;
@@ -0,0 +1,63 @@
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.migrateCodexUserHooks = migrateCodexUserHooks;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const codexHookStatus_1 = require("../codexHookStatus");
41
+ const installNativeHooks_1 = require("./installNativeHooks");
42
+ /** Run as the customer at daemon startup/repair, including silent MSI upgrades. */
43
+ async function migrateCodexUserHooks(home = os.homedir(), verify = codexHookStatus_1.getCodexHookStatus) {
44
+ const file = path.join(home, '.codex', 'hooks.json');
45
+ try {
46
+ if (!fs.existsSync(file))
47
+ return false;
48
+ const config = JSON.parse(fs.readFileSync(file, 'utf8'));
49
+ const ours = Object.values(config.hooks || {}).some((groups) => Array.isArray(groups)
50
+ && groups.some(group => Array.isArray(group?.hooks) && group.hooks.some((hook) => (0, codexHookStatus_1.isFcdCodexHook)(hook?.command))));
51
+ if (!ours)
52
+ return false;
53
+ const status = await verify();
54
+ if (status.state !== 'ready_for_probe' || !status.managed)
55
+ return false;
56
+ (0, installNativeHooks_1.installNativeHooks)({ home, clients: ['codex'], remove: true });
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ console.warn(`Codex user-hook migration deferred: ${error instanceof Error ? error.message : String(error)}`);
61
+ return false;
62
+ }
63
+ }
@@ -4,6 +4,7 @@ exports.repairProtection = repairProtection;
4
4
  const installClaudeHook_1 = require("./installClaudeHook");
5
5
  const installCursorHook_1 = require("./installCursorHook");
6
6
  const mcpGateway_1 = require("./mcpGateway");
7
+ const migrateCodexUserHooks_1 = require("./migrateCodexUserHooks");
7
8
  /**
8
9
  * Single convergence pipeline for daemon startup, drift, and remote repair.
9
10
  * MCP wrapping repairs stale runtime paths; hook repair edits only managed
@@ -13,6 +14,7 @@ async function repairProtection(args, config) {
13
14
  await (0, mcpGateway_1.protectAllCommand)({ ...args, dryRun: undefined }, config);
14
15
  const cursor = (0, installCursorHook_1.repairCursorManagedHooks)();
15
16
  const claude = (0, installClaudeHook_1.repairClaudeManagedHooks)();
17
+ await (0, migrateCodexUserHooks_1.migrateCodexUserHooks)();
16
18
  const cursorAfter = (0, installCursorHook_1.inspectCursorManagedHooks)();
17
19
  const claudeAfter = (0, installClaudeHook_1.inspectClaudeManagedHooks)();
18
20
  return {
@@ -54,6 +54,7 @@ const discoverSchedule_1 = require("./discoverSchedule");
54
54
  const installCursorHook_1 = require("./installCursorHook");
55
55
  const installClaudeHook_1 = require("./installClaudeHook");
56
56
  const installNativeHooks_1 = require("./installNativeHooks");
57
+ const installCodexManagedHooks_1 = require("./installCodexManagedHooks");
57
58
  const shellGuard_1 = require("./shellGuard");
58
59
  const posixShellGuard_1 = require("./posixShellGuard");
59
60
  const cmdGuard_1 = require("./cmdGuard");
@@ -248,6 +249,7 @@ async function runLocalDetach(args, config) {
248
249
  { label: 'Cursor runtime hooks', run: () => (0, installCursorHook_1.uninstallCursorHookCommand)({}) },
249
250
  { label: 'Claude Code / VS Code / Copilot CLI hooks', run: () => (0, installClaudeHook_1.uninstallClaudeHookCommand)({}) },
250
251
  { label: 'Codex / Gemini / Windsurf / current Kiro project hooks', run: () => { (0, installNativeHooks_1.installNativeHooks)({ remove: true }); } },
252
+ { label: 'Machine-managed Codex hooks', run: () => (0, installCodexManagedHooks_1.uninstallCodexManagedHooks)() },
251
253
  ...(process.platform === 'win32'
252
254
  ? [
253
255
  { label: 'PowerShell shell guard', run: () => (0, shellGuard_1.uninstallShellGuardCommand)() },
package/dist/index.js CHANGED
@@ -204,6 +204,11 @@ function printHelp() {
204
204
  Removes the FullCourtDefense Cursor hook entries.
205
205
  install-claude-hook
206
206
  install-native-hooks --clients codex,gemini_cli,windsurf,kiro
207
+ install-codex-managed-hooks
208
+ Administrator deployment for Codex on Windows; no employee hook approval
209
+ codex-hook-status Read Codex hook activation status (not proof of event delivery)
210
+ uninstall-codex-managed-hooks
211
+ Remove only FullCourtDefense machine-managed Codex hooks (administrator)
207
212
  Native tool hooks (Kiro: current project; Codex: trust via /hooks)
208
213
  uninstall-native-hooks
209
214
  Installs Claude-format hooks in ~/.claude/settings.json — ONE file
@@ -735,6 +740,33 @@ async function main() {
735
740
  await (0, hook_1.hookCommand)(args, config);
736
741
  break;
737
742
  }
743
+ case 'codex-hook-status': {
744
+ const { getCodexHookStatus } = await Promise.resolve().then(() => __importStar(require('./codexHookStatus')));
745
+ const status = await getCodexHookStatus();
746
+ console.log(JSON.stringify(status, null, 2));
747
+ if (status.state !== 'ready_for_probe')
748
+ process.exitCode = 1;
749
+ break;
750
+ }
751
+ case 'install-codex-managed-hooks': {
752
+ const { installCodexManagedHooks } = await Promise.resolve().then(() => __importStar(require('./commands/installCodexManagedHooks')));
753
+ const status = await installCodexManagedHooks(flags['msi-transaction']);
754
+ console.log(status.message);
755
+ console.log('Restart Codex to load the machine-managed hooks, then verify a fresh Codex action in Activity.');
756
+ break;
757
+ }
758
+ case 'rollback-codex-managed-hooks':
759
+ case 'commit-codex-managed-hooks': {
760
+ const { finishCodexManagedTransaction } = await Promise.resolve().then(() => __importStar(require('./commands/installCodexManagedHooks')));
761
+ finishCodexManagedTransaction(command === 'rollback-codex-managed-hooks', flags['msi-transaction'] || '');
762
+ break;
763
+ }
764
+ case 'uninstall-codex-managed-hooks': {
765
+ const { uninstallCodexManagedHooks } = await Promise.resolve().then(() => __importStar(require('./commands/installCodexManagedHooks')));
766
+ uninstallCodexManagedHooks();
767
+ console.log('FullCourtDefense machine-managed Codex hooks removed. Restart Codex to reload its configuration.');
768
+ break;
769
+ }
738
770
  case 'install-native-hooks':
739
771
  case 'uninstall-native-hooks': {
740
772
  const clients = flags.clients?.split(',');
@@ -743,8 +775,16 @@ async function main() {
743
775
  const files = (0, installNativeHooks_1.installNativeHooks)({ clients, project: flags.project, remove: command === 'uninstall-native-hooks' });
744
776
  for (const file of files)
745
777
  console.log(file);
746
- if (command === 'install-native-hooks')
747
- console.log('Restart the clients to load hooks. In Codex, review and trust the new definitions using /hooks. Kiro hooks apply to the selected project.');
778
+ if (command === 'install-native-hooks') {
779
+ console.log('Restart the clients to load hooks. Kiro hooks apply to the selected project.');
780
+ if (!clients || clients.includes('codex')) {
781
+ const { getCodexHookStatus } = await Promise.resolve().then(() => __importStar(require('./codexHookStatus')));
782
+ const status = await getCodexHookStatus();
783
+ console.log(status.message);
784
+ if (status.state !== 'ready_for_probe')
785
+ process.exitCode = 1;
786
+ }
787
+ }
748
788
  break;
749
789
  }
750
790
  case 'install-copilot-hook': {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.34.0"
2
+ "version": "1.34.1"
3
3
  }
@@ -0,0 +1,5 @@
1
+ export declare function isWindowsAdministrator(): boolean;
2
+ /** Parse icacls' language-independent SDDL; unknown ACL forms fail closed. */
3
+ export declare function assertProtectedDacl(sddl: string, parent?: boolean): void;
4
+ export declare function protectNewManagedDirectory(directory: string): void;
5
+ export declare function verifyManagedPath(item: string, parent?: boolean): void;
@@ -0,0 +1,132 @@
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.isWindowsAdministrator = isWindowsAdministrator;
37
+ exports.assertProtectedDacl = assertProtectedDacl;
38
+ exports.protectNewManagedDirectory = protectNewManagedDirectory;
39
+ exports.verifyManagedPath = verifyManagedPath;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const child_process_1 = require("child_process");
43
+ const windows = () => process.env.SystemRoot || 'C:\\Windows';
44
+ function run(executable, args) {
45
+ return (0, child_process_1.execFileSync)(path.join(windows(), 'System32', executable), args, {
46
+ encoding: 'utf8', windowsHide: true, timeout: 15_000,
47
+ });
48
+ }
49
+ function isWindowsAdministrator() {
50
+ if (process.platform !== 'win32')
51
+ return false;
52
+ try {
53
+ return /"S-1-16-(12288|16384)"/.test(run('whoami.exe', ['/groups', '/fo', 'csv']));
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ const trusted = new Set(['SY', 'BA', 'S-1-5-18', 'S-1-5-32-544',
60
+ 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464']);
61
+ const rights = { FA: 0x1f01ff, FR: 0x120089, FW: 0x120116, FX: 0x1200a0,
62
+ GA: 0x1f01ff, GR: 0x120089, GW: 0x120116, GX: 0x1200a0,
63
+ SD: 0x10000, RC: 0x20000, WD: 0x40000, WO: 0x80000,
64
+ CC: 1, DC: 2, LC: 4, SW: 8, RP: 16, WP: 32, DT: 64, LO: 128, CR: 256 };
65
+ /** Parse icacls' language-independent SDDL; unknown ACL forms fail closed. */
66
+ function assertProtectedDacl(sddl, parent = false) {
67
+ sddl = sddl.split('S:')[0]; // A missing audit SACL does not remove the access DACL.
68
+ if (!sddl.startsWith('D:') || sddl.includes('NO_ACCESS_CONTROL'))
69
+ throw new Error('Missing managed-path DACL.');
70
+ const aces = sddl.match(/\([^()]*\)/g) || [];
71
+ if (!aces.length)
72
+ throw new Error('Empty or unsupported managed-path DACL.');
73
+ for (const ace of aces) {
74
+ const fields = ace.slice(1, -1).split(';');
75
+ if (fields.length !== 6)
76
+ throw new Error('Unsupported managed-path ACL.');
77
+ const [type, flags, mask, , , sid] = fields;
78
+ if (flags.includes('IO') || type === 'D')
79
+ continue;
80
+ if (type !== 'A')
81
+ throw new Error('Unsupported managed-path ACL entry.');
82
+ if (trusted.has(sid))
83
+ continue;
84
+ let value = 0;
85
+ if (/^0x[0-9a-f]+$/i.test(mask))
86
+ value = Number.parseInt(mask.slice(2), 16);
87
+ else {
88
+ if (!mask || mask.length % 2)
89
+ throw new Error('Unsupported managed-path access rights.');
90
+ for (let i = 0; i < mask.length; i += 2) {
91
+ const right = rights[mask.slice(i, i + 2)];
92
+ if (right === undefined)
93
+ throw new Error('Unsupported managed-path access rights.');
94
+ value |= right;
95
+ }
96
+ }
97
+ if ((value & (parent ? 0xd0040 : 0xd0156)) || (value & 0x50000000)) {
98
+ throw new Error('Managed capture requires administrator-only write access to its configuration and runtime.');
99
+ }
100
+ }
101
+ }
102
+ function protectNewManagedDirectory(directory) {
103
+ run('icacls.exe', [directory, '/inheritance:r', '/grant:r',
104
+ '*S-1-5-18:(OI)(CI)F', '*S-1-5-32-544:(OI)(CI)F', '*S-1-5-32-545:(OI)(CI)RX', '/q']);
105
+ }
106
+ function verifyManagedPath(item, parent = false) {
107
+ if (fs.lstatSync(item).isSymbolicLink())
108
+ throw new Error('Managed capture refuses redirected paths.');
109
+ // Windows Temp permits users to create their own children, not replace an
110
+ // administrator's existing child. Never use a user's TEMP for SYSTEM input.
111
+ const scratch = fs.mkdtempSync(path.join(windows(), 'Temp', 'fcd-managed-acl-'));
112
+ const saved = path.join(scratch, 'acl.txt');
113
+ try {
114
+ run('icacls.exe', [item, '/save', saved, '/q']);
115
+ const lines = fs.readFileSync(saved).toString('utf16le').replace(/^\uFEFF/, '').split(/\r?\n/);
116
+ const dacl = lines.find(line => line.startsWith('D:'));
117
+ try {
118
+ assertProtectedDacl(dacl || '', parent);
119
+ }
120
+ catch (error) {
121
+ throw new Error(`${item}: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ // Ownership grants implicit WRITE_DAC. Transfer only these managed config
124
+ // and FCD runtime paths to Administrators, preserving their existing DACL.
125
+ run('icacls.exe', [item, '/setowner', '*S-1-5-32-544', '/q']);
126
+ }
127
+ finally {
128
+ if (fs.existsSync(saved))
129
+ fs.unlinkSync(saved);
130
+ fs.rmdirSync(scratch);
131
+ }
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "test:native-hooks": "npm run build && node scripts/test-native-hook-launch.js && node scripts/test-native-hook-edges.js && node scripts/test-client-telemetry.js && node scripts/test-native-hook-clients.js",
16
+ "test:native-hooks": "npm run build && node scripts/test-codex-activation.js && node scripts/test-native-hook-launch.js && node scripts/test-native-hook-edges.js && node scripts/test-client-telemetry.js && node scripts/test-native-hook-clients.js",
17
17
  "test:endpoint-approvals": "npm run build && node scripts/audit-endpoint-mcp-shell.js",
18
18
  "test:endpoint-clients": "npm run build && node scripts/test-endpoint-client-matrix.js && node scripts/test-copilot-hook-install.js",
19
19
  "build": "tsc && node scripts/copy-attack-corpus.js",