/** * The emitted Codex helper bodies (`crossrt-2-codex-hooks`, ADR-003 / ADR-004). * * Generated-code-as-a-string, following the `generateAgentdbWriter` precedent (`setup.ts`): the * helpers are THIN and the logic lives in the package, because a changed helper body changes * codex's `currentHash` and disarms the entry until it is re-trusted (MEASURED — M0 spike §4). * Every byte in here is therefore a liability; keep them boring. * * ## The contracts, side by side * * | | `dz-codex-veto.cjs` (PreToolUse) | `dz-codex-recall.cjs` (UserPromptSubmit) | * |----------------|-----------------------------------------------------|------------------------------------------| * | Polarity | mode-driven on POLICY (default `warn` ⇒ exit 0) | **never-block, always** | * | Keys on | presence of `tool_input.command` (AM-8) | `payload.prompt` | * | Activation | nearest `.dz` walking up from `payload.cwd` (AM-25) | same | * | Our own failure| exit 0, silent, note to `helper-errors.jsonl` | exit 0, empty stdout, no write (AM-9) | * * `.git` is **not** an activation marker (AM-25): a user-global hook that treats "any git checkout" * as opted-in reaches every repository on the machine, including trees nobody pointed at dz. * * The self-failure note goes to `$CODEX_HOME/dz-hooks/helper-errors.jsonl` (AM-33), never into a * project — writing it into `/.dz/` would CREATE a `.dz/` in a foreign repo, which is the * exact thing the activation rule exists to prevent. * * A `UserPromptSubmit` hook that exits 2 **blocks the user's turn**, so the recall helper has no * path to a non-zero exit at all. * * @packageDocumentation */ import { DZ_HOOK_HELPER_VERSION } from './codex-hooks.js'; /** Shared prologue: payload read, project-root walk, never-throw error note. */ function preamble(): string { return `#!/usr/bin/env node // GENERATED by @dzhechkov/harness-core — dz-hook-helper-version: ${DZ_HOOK_HELPER_VERSION} // Do not edit: dz rewrites this file, and any edit disarms the hook until it is re-trusted. 'use strict'; const fs = require('node:fs'); const path = require('node:path'); const HELPER_VERSION = ${DZ_HOOK_HELPER_VERSION}; const CODEX_HOME = process.env.CODEX_HOME || path.join(require('node:os').homedir(), '.codex'); const ERROR_LOG = path.join(CODEX_HOME, 'dz-hooks', 'helper-errors.jsonl'); /** * Append one line to the helper NOTES log. Never throws. Never writes into a project. * * Carries BOTH self-failures and policy WARNINGS. MEASURED (M9 live probe, 2026-08-19): codex * surfaces a hook's stderr in the transcript ONLY when the hook exits 2 — an exit-0 hook's stderr is * swallowed. So a warn that lived only on stderr would be unobservable, and "the guard is live but * not blocking" (G-J) would be unprovable. This file is where it is observable. * * The log is 0600 inside a 0700 directory, ENFORCED on every write rather than assumed at creation: * \`mkdir -p\` never relaxes an existing directory's mode, so a helper dir that predates dz (or a * permissive umask) would leave the record world-readable (independent review, finding 8). */ function note(stage, err, extra) { try { try { fs.mkdirSync(path.dirname(ERROR_LOG), { recursive: true, mode: 0o700 }); } catch (_) { /* exists */ } try { fs.chmodSync(path.dirname(ERROR_LOG), 0o700); } catch (_) { /* not ours to tighten */ } try { if (!fs.existsSync(ERROR_LOG)) fs.writeFileSync(ERROR_LOG, '', { mode: 0o600 }); } catch (_) { /* raced */ } try { fs.chmodSync(ERROR_LOG, 0o600); } catch (_) { /* not ours to tighten */ } fs.appendFileSync( ERROR_LOG, JSON.stringify(Object.assign( { ts: new Date().toISOString(), helper: HELPER, helperVersion: HELPER_VERSION, stage, message: String((err && err.message) || err || '') }, extra || {}, )) + '\\n', ); } catch (_) { /* the note is best-effort; it must never become the failure */ } } /** * What a note may say about a COMMAND: its hash, its program name, and a probe nonce if one is * embedded — never the line itself. * * A veto fires on exactly the commands most likely to carry a credential (\`ssh\`, \`curl -u\`, * \`psql\`), and the note is a durable file in the user's home. It recorded the full line * (independent review, finding 8). The hash keeps the record useful — two notes are the same * command iff their hashes match — and \`probeNonce\` keeps the live probe gradeable, because the * probe's own correlator is a purpose-built token (\`dzprobe-…\` / \`dzverify-…\`) and not a secret. */ function commandFacts(command) { var facts = {}; try { facts.commandSha256 = require('node:crypto').createHash('sha256').update(String(command), 'utf8').digest('hex'); } catch (_) { /* hashing is best-effort too */ } try { // The first token is a BINARY NAME or nothing. \`SECRET=xyz ssh …\` puts a credential in the // first token, and a synopsis is not worth one byte of it (fix round 2, R2-8): anything that is // not a plain program name — in particular anything carrying \`=\` — is redacted outright. var first = String(command).trim().split(/\\s+/)[0] || ''; facts.commandSynopsis = /^[A-Za-z0-9._\\/-]+$/.test(first) ? first.slice(0, 40) : '(redacted)'; } catch (_) { /* ignore */ } try { var m = String(command).match(/dz(?:probe|verify)-[0-9a-zA-Z-]+/); if (m) facts.probeNonce = m[0]; } catch (_) { /* ignore */ } return facts; } function readPayload() { try { const raw = fs.readFileSync(0, 'utf8'); const parsed = JSON.parse(raw); return parsed && typeof parsed === 'object' ? parsed : null; } catch (err) { note('read-payload', err); return null; } } /** * Walk UP from cwd to the nearest directory containing a \`.dz\` DIRECTORY. * \`.git\` is deliberately not a marker (AM-25). No root => the helper is inert. */ function findProjectRoot(startDir) { try { let dir = path.resolve(startDir); for (let i = 0; i < 64; i += 1) { try { if (fs.statSync(path.join(dir, '.dz')).isDirectory()) return dir; } catch (_) { /* not here; keep walking */ } const parent = path.dirname(dir); if (parent === dir) return null; dir = parent; } return null; } catch (err) { note('find-root', err); return null; } } function readProjectConfig(root) { try { return JSON.parse(fs.readFileSync(path.join(root, '.dz', 'config.json'), 'utf8')); } catch (_) { return null; // absent or unreadable config is the DEFAULT case, not an error } } /** Load a built module from the project's own harness-core, or null. Never throws. */ function loadCore(root, fileName, predicate) { const candidates = [ path.join(root, 'node_modules', '@dzhechkov', 'harness-core', 'dist', fileName), path.join(root, 'packages', '@dzhechkov', 'harness-core', 'dist', fileName), ]; for (const candidate of candidates) { try { if (!fs.existsSync(candidate)) continue; const mod = require(candidate); if (predicate(mod)) return mod; } catch (err) { note('load-core:' + fileName, err); } } return null; } `; } /** * The PreToolUse veto helper. * * Fail-OPEN on OUR failure, mode-driven on a POLICY hit. Exit 2 only when the project explicitly * opted in — the shipped default warns and returns 0 (AM-24 / G-J). */ export function generateCodexVetoHelper(): string { return `${preamble().replace('const HELPER_VERSION', "const HELPER = 'dz-codex-veto';\nconst HELPER_VERSION")} function main() { const payload = readPayload(); if (payload === null) return 0; // AM-8: key on the PRESENCE of a command, never on tool_name equality. MEASURED payload // (M0 spike): { tool_name: 'Bash', tool_input: { command: "…" }, cwd: "…" }. const input = payload.tool_input; const command = input && typeof input === 'object' ? input.command : undefined; if (typeof command !== 'string' || command === '') return 0; const cwd = typeof payload.cwd === 'string' && payload.cwd !== '' ? payload.cwd : process.env.PWD || process.cwd(); const root = findProjectRoot(cwd); if (root === null) return 0; // inert outside an opted-in dz project: no decision, no output, no write const policy = loadCore(root, 'shell-veto-policy.js', (m) => typeof m.vetoShellCommand === 'function'); if (policy === null) return 0; // no logic available => allow (never block on OUR failure) let hit = null; let mode = 'warn'; try { hit = policy.vetoShellCommand(command); mode = policy.resolveVetoMode(readProjectConfig(root)); } catch (err) { note('policy', err); return 0; } if (hit === null || mode === 'off') return 0; if (mode === 'block') { process.stderr.write('DZ-VETO: ' + hit.rule + ' — ' + hit.reason + ' — command: ' + command + '\\n'); note('veto-block', null, Object.assign({ marker: 'DZ-VETO', rule: hit.rule, mode: mode }, commandFacts(command))); return 2; } const warnLine = 'DZ-VETO-WARN: ' + hit.rule + ' — ' + hit.reason + ' — command: ' + command; process.stderr.write(warnLine + '\\n'); // Codex swallows an exit-0 hook's stderr (MEASURED), so the warn is ALSO recorded where it can be // read back — otherwise the shipped default would be live-but-unobservable. note('veto-warn', null, Object.assign({ marker: 'DZ-VETO-WARN', rule: hit.rule, mode: mode }, commandFacts(command))); return 0; } let code = 0; try { code = main(); } catch (err) { note('main', err); code = 0; } process.exit(code === 2 ? 2 : 0); `; } /** * The UserPromptSubmit recall helper. * * NEVER blocks and NEVER exits non-zero (AM-9). Silence is its correct output when nothing is * relevant, which is exactly why the acceptance canary is a FORCED HIT plus a removed-hook twin * (AM-4) — a dead hook and a correctly-silent one are indistinguishable from the outside. */ export function generateCodexRecallHelper(): string { return `${preamble().replace('const HELPER_VERSION', "const HELPER = 'dz-codex-recall';\nconst HELPER_VERSION")} const net = require('node:net'); const DAEMON_TIMEOUT_MS = 2000; /** Ask the project's embed daemon for candidates. Resolves undefined on ANY problem. */ function askDaemon(root, prompt) { return new Promise((resolve) => { const socket = process.env.DZ_EMBED_SOCKET || path.join(root, '.dz', 'embed.sock'); let exists = false; try { exists = fs.existsSync(socket); } catch (_) { exists = false; } if (!exists) return resolve(undefined); let settled = false; const done = (v) => { if (settled) return; settled = true; try { sock.destroy(); } catch (_) { /* ignore */ } resolve(v); }; const sock = net.connect(socket); const timer = setTimeout(() => done(undefined), DAEMON_TIMEOUT_MS); if (typeof timer.unref === 'function') timer.unref(); let buf = ''; sock.on('connect', () => sock.write(JSON.stringify({ op: 'recall', prompt: prompt, limit: 8 }) + '\\n')); sock.on('data', (chunk) => { buf += chunk.toString('utf8'); const nl = buf.indexOf('\\n'); if (nl === -1) return; clearTimeout(timer); let msg = null; try { msg = JSON.parse(buf.slice(0, nl)); } catch (_) { msg = null; } done(msg && Array.isArray(msg.hits) ? msg.hits : undefined); }); sock.on('error', () => { clearTimeout(timer); done(undefined); }); }); } async function main() { const payload = readPayload(); if (payload === null) return; const prompt = typeof payload.prompt === 'string' ? payload.prompt : ''; if (prompt.trim() === '') return; const cwd = typeof payload.cwd === 'string' && payload.cwd !== '' ? payload.cwd : process.env.PWD || process.cwd(); const root = findProjectRoot(cwd); if (root === null) return; // inert outside an opted-in dz project const policy = loadCore(root, 'recall-hook-policy.js', (m) => typeof m.selectHookHits === 'function'); if (policy === null) return; const candidates = await askDaemon(root, prompt); if (!candidates || candidates.length === 0) return; // daemon dead or nothing relevant: silence let selection = null; try { selection = policy.selectHookHits(prompt, candidates); } catch (err) { note('select-hits', err); return; } if (!selection || !Array.isArray(selection.hits) || selection.hits.length === 0) return; let context = ''; try { context = policy.renderHookContext(selection); } catch (err) { note('render', err); return; } if (context === '') return; // empty context => print NOTHING (an empty block is noise) process.stdout.write( JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } }) + '\\n', ); // The usage row carries runtime: 'codex' (ADR-003 §3) through the SHARED chained appender in // harness-core — ONE writer implementation, two callers (AM-6/AM-7). Written AFTER stdout so a // logging failure can never cost the user their injection. const usage = loadCore(root, 'recall-usage.js', (m) => typeof m.appendRecallUsage === 'function'); if (usage === null) return; try { usage.appendRecallUsage({ projectRoot: root, runtime: 'codex', query: prompt, runId: typeof payload.session_id === 'string' ? payload.session_id : undefined, hits: selection.hits.map((h) => ({ dzId: h.dzId, score: h.score })), }); } catch (err) { note('append-usage', err); } } main() .catch((err) => note('main', err)) .finally(() => process.exit(0)); // a UserPromptSubmit exit 2 BLOCKS the user's turn `; } /** Both helper bodies, keyed by the file they are written to. */ export function generateCodexHelpers(): Readonly> { return { veto: generateCodexVetoHelper(), recall: generateCodexRecallHelper() }; }