From ae0cc075b65f013e53feae21b514ecdda942e272 Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:06:43 -0400 Subject: [PATCH 1/7] docs: prepare HOL Guard before-tool adapter contract --- docs/hol-guard-before-tool.md | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/hol-guard-before-tool.md diff --git a/docs/hol-guard-before-tool.md b/docs/hol-guard-before-tool.md new file mode 100644 index 0000000..c323965 --- /dev/null +++ b/docs/hol-guard-before-tool.md @@ -0,0 +1,79 @@ +# HOL Guard `before_tool` adapter packet + +Status: owned-fork preparation only. This is not an upstream integration or a release artifact. + +## Why this maps cleanly + +Klaat Code sends a JSON object to `before_tool` hooks. For a `run_command` call, the relevant fields are: + +- `event`: `before_tool` +- `project_root`: current project root +- `tool_name`: `run_command` +- `tool_args`: the raw JSON argument string passed to the tool + +Klaat Code blocks a tool call when the hook exits with status `2` or writes a JSON object such as: + +```json +{"decision":"block","reason":"..."} +``` + +HOL Guard already has native pre-tool authority for command decisions. The supported native request is a versioned `PreToolUse` envelope whose `payload.tool_input.command` contains the command string. The Rust runtime owns the security decision. An adapter must transport and render that result, not reimplement Guard policy from Klaat Code. + +## Required adapter behavior + +For `tool_name == "run_command"`: + +1. Parse `tool_args` as JSON. +2. Extract the `command` string. If it is absent or malformed, fail closed for this matched hook rather than guessing. +3. Construct a Guard `PreToolUse` request using the current project as `cwd`. +4. Send the request through HOL Guard's native hook authority. +5. If Guard returns a deny/block/review/reapproval/sandbox-required floor that prevents execution, translate it to Klaat Code's block contract. +6. If Guard returns an allow result, exit successfully without a block response. +7. If Guard cannot produce an authoritative decision, fail closed. Do not silently allow a command because Guard is unavailable. + +Do not use `hol-guard command explain` as the enforcement authority. It is useful for stateless inspection and test assertions, but the live hook decision must remain native-authoritative. + +## Minimum validation cases + +The implementation is not ready for upstream submission until these cases are exercised against the real Guard runtime: + +| Case | Expected result | +| --- | --- | +| `pwd` | Klaat Code command proceeds | +| `git status --short` | Klaat Code command proceeds | +| `rm -rf /` | Klaat Code command is blocked | +| `shred ~/.ssh/id_ed25519` | Klaat Code command is blocked | +| malformed `tool_args` | fail closed | +| Guard runtime unavailable | fail closed | + +The deny-path test must prove that the target command is not executed. + +## Candidate hook configuration + +The adapter should be project-local until there is a stable shipped integration surface: + +```json +{ + "before_tool": [ + { + "matcher": "run_command", + "command": "./scripts/hol-guard-before-tool" + } + ] +} +``` + +The current README example points at `./scripts/guard-shell.sh`, but that helper is not present in the repository. A real contribution should either ship the referenced helper or replace the example with an existing supported command. + +## Upstream gate + +Before any upstream PR: + +- revalidate the current Klaat Code hook payload and block semantics; +- revalidate HOL Guard's current native hook entry point; +- implement the smallest portable adapter with no duplicated policy logic; +- run Klaat Code typecheck/build plus focused adapter tests; +- prove one allow path and one deny path with the real Guard runtime; +- confirm the contribution still fits current maintainer guidance and Distribution WIP limits. + +Until those gates pass, this packet is preparation only and carries no strict outcome value. From 6788d48f8145eccc116bf96bd1238dade7ce378a Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:49:07 -0400 Subject: [PATCH 2/7] feat: add HOL Guard before-tool adapter --- scripts/hol-guard-before-tool.mjs | 160 ++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 scripts/hol-guard-before-tool.mjs diff --git a/scripts/hol-guard-before-tool.mjs b/scripts/hol-guard-before-tool.mjs new file mode 100644 index 0000000..ccf814b --- /dev/null +++ b/scripts/hol-guard-before-tool.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; +import process from 'node:process'; + +const MAX_STDIN_BYTES = 1_000_000; +const GUARD_TIMEOUT_MS = 9_000; + +function block(reason) { + const message = typeof reason === 'string' && reason.trim() + ? reason.trim() + : 'HOL Guard did not return an authoritative allow decision.'; + process.stdout.write(`${JSON.stringify({ decision: 'block', reason: message })}\n`); +} + +export function extractKlaatCommand(payload) { + if (!payload || typeof payload !== 'object') { + throw new Error('invalid Klaat hook payload'); + } + if (payload.event !== 'before_tool' || payload.tool_name !== 'run_command') { + return null; + } + if (typeof payload.tool_args !== 'string') { + throw new Error('run_command tool_args must be a JSON string'); + } + let args; + try { + args = JSON.parse(payload.tool_args); + } catch { + throw new Error('run_command tool_args is not valid JSON'); + } + if (!args || typeof args !== 'object' || typeof args.command !== 'string' || !args.command.trim()) { + throw new Error('run_command command is missing'); + } + return args.command; +} + +export function buildGuardPayload(payload, command) { + const guardPayload = { + hook_event_name: 'PreToolUse', + tool_name: 'Bash', + tool_input: { command }, + }; + if (typeof payload.session_id === 'string' && payload.session_id) { + guardPayload.session_id = payload.session_id; + } + if (typeof payload.project_root === 'string' && payload.project_root) { + guardPayload.cwd = payload.project_root; + } + return guardPayload; +} + +function parseGuardJson(stdout) { + const text = typeof stdout === 'string' ? stdout.trim() : ''; + if (!text) return null; + try { + return JSON.parse(text); + } catch { + for (const line of text.split(/\r?\n/).reverse()) { + const candidate = line.trim(); + if (!candidate) continue; + try { + const parsed = JSON.parse(candidate); + if (parsed && typeof parsed === 'object') return parsed; + } catch { + // Keep looking for the final JSON response line. + } + } + return null; + } +} + +export function decisionFromGuardResponse(response) { + if (!response || typeof response !== 'object') { + return { allow: false, reason: 'HOL Guard returned an invalid response.' }; + } + const hookSpecific = response.hookSpecificOutput; + const permissionDecision = hookSpecific && typeof hookSpecific === 'object' + ? hookSpecific.permissionDecision + : undefined; + const policyAction = response.policy_action; + const reasonCode = response.reason_code; + + if ( + permissionDecision === 'allow' && + (policyAction === 'allow' || policyAction === 'warn') && + reasonCode !== 'harness_not_managed' + ) { + return { allow: true, reason: '' }; + } + + const reason = + (typeof response.reason === 'string' && response.reason) || + (hookSpecific && typeof hookSpecific === 'object' && + typeof hookSpecific.permissionDecisionReason === 'string' && + hookSpecific.permissionDecisionReason) || + (typeof reasonCode === 'string' && `HOL Guard blocked this command (${reasonCode}).`) || + 'HOL Guard did not return an authoritative allow decision.'; + return { allow: false, reason }; +} + +async function readStdin() { + const chunks = []; + let size = 0; + for await (const chunk of process.stdin) { + size += chunk.length; + if (size > MAX_STDIN_BYTES) throw new Error('Klaat hook payload exceeds the size limit'); + chunks.push(chunk); + } + return Buffer.concat(chunks).toString('utf8'); +} + +export async function main() { + let payload; + try { + const input = await readStdin(); + payload = JSON.parse(input); + } catch (error) { + block(error instanceof Error ? error.message : 'invalid Klaat hook payload'); + return; + } + + let command; + try { + command = extractKlaatCommand(payload); + } catch (error) { + block(error instanceof Error ? error.message : 'invalid run_command payload'); + return; + } + if (command === null) return; + + const guardPayload = buildGuardPayload(payload, command); + const args = ['hook', '--harness', 'codex']; + if (typeof payload.project_root === 'string' && payload.project_root) { + args.push('--workspace', payload.project_root); + } + args.push('--json'); + + const result = spawnSync('hol-guard', args, { + input: `${JSON.stringify(guardPayload)}\n`, + encoding: 'utf8', + timeout: GUARD_TIMEOUT_MS, + maxBuffer: MAX_STDIN_BYTES, + windowsHide: true, + }); + + if (result.error || result.status !== 0) { + block('HOL Guard could not complete an authoritative command review.'); + return; + } + + const response = parseGuardJson(result.stdout); + const decision = decisionFromGuardResponse(response); + if (!decision.allow) block(decision.reason); +} + +const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : ''; +if (import.meta.url === invokedPath) { + await main(); +} From a7958652fba2b27d8782da43d81dc7f27cce625e Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:49:50 -0400 Subject: [PATCH 3/7] test: cover HOL Guard before-tool adapter --- scripts/hol-guard-before-tool.test.mjs | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 scripts/hol-guard-before-tool.test.mjs diff --git a/scripts/hol-guard-before-tool.test.mjs b/scripts/hol-guard-before-tool.test.mjs new file mode 100644 index 0000000..80d16c2 --- /dev/null +++ b/scripts/hol-guard-before-tool.test.mjs @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + buildGuardPayload, + decisionFromGuardResponse, + extractKlaatCommand, +} from './hol-guard-before-tool.mjs'; + +test('extracts run_command command from Klaat tool_args', () => { + assert.equal(extractKlaatCommand({ + event: 'before_tool', + tool_name: 'run_command', + tool_args: JSON.stringify({ command: 'git status --short' }), + }), 'git status --short'); +}); + +test('ignores unrelated hook events defensively', () => { + assert.equal(extractKlaatCommand({ event: 'after_tool', tool_name: 'run_command', tool_args: '{}' }), null); +}); + +test('fails closed on malformed run_command input', () => { + assert.throws(() => extractKlaatCommand({ + event: 'before_tool', + tool_name: 'run_command', + tool_args: '{', + })); +}); + +test('builds complete PreToolUse command envelope', () => { + assert.deepEqual( + buildGuardPayload({ session_id: 's1', project_root: '/repo' }, 'pwd'), + { + hook_event_name: 'PreToolUse', + tool_name: 'Bash', + tool_input: { command: 'pwd' }, + session_id: 's1', + cwd: '/repo', + }, + ); +}); + +test('allows only native allow or warn decisions rendered as allow', () => { + assert.equal(decisionFromGuardResponse({ + policy_action: 'allow', + reason_code: 'native_pre_tool_allow', + hookSpecificOutput: { permissionDecision: 'allow' }, + }).allow, true); + assert.equal(decisionFromGuardResponse({ + policy_action: 'warn', + reason_code: 'native_policy_warning', + hookSpecificOutput: { permissionDecision: 'allow' }, + }).allow, true); +}); + +test('blocks review, block, malformed, and unmanaged passthrough responses', () => { + for (const response of [ + { policy_action: 'review', hookSpecificOutput: { permissionDecision: 'deny' } }, + { policy_action: 'block', hookSpecificOutput: { permissionDecision: 'deny' } }, + { policy_action: 'allow', reason_code: 'harness_not_managed', hookSpecificOutput: { permissionDecision: 'allow' } }, + {}, + ]) { + assert.equal(decisionFromGuardResponse(response).allow, false); + } +}); From f862e5e681ff976666286d7db077ffabbac3fd53 Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:50:14 -0400 Subject: [PATCH 4/7] docs: record HOL Guard adapter preparation --- docs/hol-guard-before-tool.md | 59 ++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/docs/hol-guard-before-tool.md b/docs/hol-guard-before-tool.md index c323965..53d4a2d 100644 --- a/docs/hol-guard-before-tool.md +++ b/docs/hol-guard-before-tool.md @@ -1,6 +1,6 @@ # HOL Guard `before_tool` adapter packet -Status: owned-fork preparation only. This is not an upstream integration or a release artifact. +Status: owned-fork implementation and tests prepared. This is not an upstream integration or a release artifact. ## Why this maps cleanly @@ -17,23 +17,39 @@ Klaat Code blocks a tool call when the hook exits with status `2` or writes a JS {"decision":"block","reason":"..."} ``` -HOL Guard already has native pre-tool authority for command decisions. The supported native request is a versioned `PreToolUse` envelope whose `payload.tool_input.command` contains the command string. The Rust runtime owns the security decision. An adapter must transport and render that result, not reimplement Guard policy from Klaat Code. +HOL Guard already has native pre-tool authority for command decisions. The supported native request is a versioned `PreToolUse` envelope whose `tool_input.command` contains the command string. The Rust runtime owns the security decision. The adapter transports and renders that result rather than reimplementing Guard policy in Klaat Code. -## Required adapter behavior +## Prepared adapter behavior -For `tool_name == "run_command"`: +The owned fork now contains `scripts/hol-guard-before-tool.mjs`. For `tool_name == "run_command"` it: -1. Parse `tool_args` as JSON. -2. Extract the `command` string. If it is absent or malformed, fail closed for this matched hook rather than guessing. -3. Construct a Guard `PreToolUse` request using the current project as `cwd`. -4. Send the request through HOL Guard's native hook authority. -5. If Guard returns a deny/block/review/reapproval/sandbox-required floor that prevents execution, translate it to Klaat Code's block contract. -6. If Guard returns an allow result, exit successfully without a block response. -7. If Guard cannot produce an authoritative decision, fail closed. Do not silently allow a command because Guard is unavailable. +1. Parses `tool_args` as JSON. +2. Extracts the non-empty `command` string. Missing or malformed command input fails closed. +3. Constructs a Guard `PreToolUse` request with the current project as `cwd`. +4. Sends the complete request through `hol-guard hook --harness codex --json`, with the workspace when available, so Guard's native hook authority remains the decision owner. +5. Allows only a native-rendered `allow` or `warn` result whose hook permission decision is `allow`. +6. Maps review, reapproval, sandbox-required, block, malformed output, unmanaged-harness passthrough, timeout, missing Guard, or any other non-authoritative state to Klaat Code's JSON block contract. -Do not use `hol-guard command explain` as the enforcement authority. It is useful for stateless inspection and test assertions, but the live hook decision must remain native-authoritative. +The adapter does not use `hol-guard command explain` as enforcement authority. -## Minimum validation cases +## Local preparation proof + +`node --check scripts/hol-guard-before-tool.mjs` passed before the owned-fork write. + +`node --test scripts/hol-guard-before-tool.test.mjs` passed six focused unit tests covering: + +- command extraction; +- unrelated hook pass-through; +- malformed `tool_args` fail-closed behavior; +- complete `PreToolUse` envelope construction; +- native allow/warn handling; +- review/block/unmanaged/malformed response rejection. + +A mocked `hol-guard` executable also proved the adapter's process boundary for allow, deny, malformed Klaat input, and unavailable Guard behavior. + +These are preparation checks only. They are not a substitute for real HOL Guard runtime proof. + +## Minimum real-runtime validation cases The implementation is not ready for upstream submission until these cases are exercised against the real Guard runtime: @@ -50,30 +66,29 @@ The deny-path test must prove that the target command is not executed. ## Candidate hook configuration -The adapter should be project-local until there is a stable shipped integration surface: +The adapter should remain project-local until there is a stable shipped integration surface: ```json { "before_tool": [ { "matcher": "run_command", - "command": "./scripts/hol-guard-before-tool" + "command": "node ./scripts/hol-guard-before-tool.mjs" } ] } ``` -The current README example points at `./scripts/guard-shell.sh`, but that helper is not present in the repository. A real contribution should either ship the referenced helper or replace the example with an existing supported command. +The current upstream README example points at `./scripts/guard-shell.sh`, but that helper is not present in the repository. This owned-fork candidate supplies the missing executable behavior without changing Klaat's hook semantics. ## Upstream gate Before any upstream PR: -- revalidate the current Klaat Code hook payload and block semantics; +- revalidate the current Klaat Code upstream head, hook payload, block semantics, contribution guidance, and queue ownership; - revalidate HOL Guard's current native hook entry point; -- implement the smallest portable adapter with no duplicated policy logic; -- run Klaat Code typecheck/build plus focused adapter tests; -- prove one allow path and one deny path with the real Guard runtime; -- confirm the contribution still fits current maintainer guidance and Distribution WIP limits. +- run Klaat Code typecheck/build plus the focused adapter tests; +- prove one allow path and one deny path with the real Guard runtime, including target command non-execution on deny; +- confirm the contribution still fits current Distribution WIP and maintainer-conversion gates. -Until those gates pass, this packet is preparation only and carries no strict outcome value. +Until those gates pass, this remains owned-fork preparation and carries no strict outcome value. From ed8d589808ddd5a564bed69d75803ea17e3a6662 Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:13:56 -0400 Subject: [PATCH 5/7] fix: fail closed on unavailable Guard decisions --- scripts/hol-guard-before-tool.mjs | 62 +++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/scripts/hol-guard-before-tool.mjs b/scripts/hol-guard-before-tool.mjs index ccf814b..956c626 100644 --- a/scripts/hol-guard-before-tool.mjs +++ b/scripts/hol-guard-before-tool.mjs @@ -6,6 +6,36 @@ import process from 'node:process'; const MAX_STDIN_BYTES = 1_000_000; const GUARD_TIMEOUT_MS = 9_000; +const NON_AUTHORITATIVE_REASON_CODES = new Set([ + 'native_hook_disabled', + 'native_shadow_diagnostic_disabled', + 'native_policy_not_ready', + 'native_hook_event_unavailable', + 'native_pre_tool_unavailable', + 'native_post_tool_unavailable', + 'native_overloaded', + 'native_hook_worker_unavailable', + 'native_hook_worker_unavailable_before_compatibility', + 'native_hook_worker_unsupported', + 'native_hook_worker_exception', + 'native_hook_compatibility_disabled', + 'native_hook_edge_invalid_response', + 'native_hook_edge_unavailable', + 'python_hook_oracle_unavailable', + 'python_oracle_exception', + 'watch_recording_only', + 'daemon_hook_queue_capacity', + 'daemon_hook_deadline_exhausted', + 'daemon_hook_process_deadline_exhausted', + 'daemon_hook_process_not_ready', + 'daemon_hook_process_failed', + 'daemon_hook_process_invalid_request', + 'daemon_hook_process_guard_home_mismatch', + 'daemon_worker_exception', + 'harness_not_managed', + 'native_degraded_emergency_safe', +]); + function block(reason) { const message = typeof reason === 'string' && reason.trim() ? reason.trim() @@ -79,23 +109,33 @@ export function decisionFromGuardResponse(response) { ? hookSpecific.permissionDecision : undefined; const policyAction = response.policy_action; - const reasonCode = response.reason_code; - - if ( - permissionDecision === 'allow' && - (policyAction === 'allow' || policyAction === 'warn') && - reasonCode !== 'harness_not_managed' - ) { - return { allow: true, reason: '' }; - } - + const reasonCode = typeof response.reason_code === 'string' + ? response.reason_code.trim() + : ''; const reason = (typeof response.reason === 'string' && response.reason) || (hookSpecific && typeof hookSpecific === 'object' && typeof hookSpecific.permissionDecisionReason === 'string' && hookSpecific.permissionDecisionReason) || - (typeof reasonCode === 'string' && `HOL Guard blocked this command (${reasonCode}).`) || + (reasonCode && `HOL Guard blocked this command (${reasonCode}).`) || 'HOL Guard did not return an authoritative allow decision.'; + + if (permissionDecision !== 'allow' || !reasonCode) { + return { allow: false, reason }; + } + + if (NON_AUTHORITATIVE_REASON_CODES.has(reasonCode)) { + return { allow: false, reason }; + } + + if (policyAction === 'allow') { + return { allow: true, reason: '' }; + } + + if (policyAction === 'warn' && reasonCode === 'native_policy_warning') { + return { allow: true, reason: '' }; + } + return { allow: false, reason }; } From e0d553c66fa97d3bba39e0c86962a968ff85300a Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:14:11 -0400 Subject: [PATCH 6/7] test: cover unavailable Guard fail-closed path --- scripts/hol-guard-before-tool.test.mjs | 29 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/hol-guard-before-tool.test.mjs b/scripts/hol-guard-before-tool.test.mjs index 80d16c2..ed107b3 100644 --- a/scripts/hol-guard-before-tool.test.mjs +++ b/scripts/hol-guard-before-tool.test.mjs @@ -39,10 +39,10 @@ test('builds complete PreToolUse command envelope', () => { ); }); -test('allows only native allow or warn decisions rendered as allow', () => { +test('allows authoritative native allow and policy warning decisions', () => { assert.equal(decisionFromGuardResponse({ policy_action: 'allow', - reason_code: 'native_pre_tool_allow', + reason_code: 'native_exact_safe_command', hookSpecificOutput: { permissionDecision: 'allow' }, }).allow, true); assert.equal(decisionFromGuardResponse({ @@ -52,11 +52,28 @@ test('allows only native allow or warn decisions rendered as allow', () => { }).allow, true); }); -test('blocks review, block, malformed, and unmanaged passthrough responses', () => { +test('fails closed on unavailable and non-authoritative warn decisions', () => { + for (const reason_code of [ + 'native_pre_tool_unavailable', + 'native_policy_not_ready', + 'native_hook_worker_unavailable', + 'harness_not_managed', + 'future_unknown_warning', + ]) { + assert.equal(decisionFromGuardResponse({ + policy_action: 'warn', + reason_code, + reason: 'Guard did not complete an authoritative review.', + hookSpecificOutput: { permissionDecision: 'allow' }, + }).allow, false, reason_code); + } +}); + +test('blocks review, block, malformed, and missing-reason responses', () => { for (const response of [ - { policy_action: 'review', hookSpecificOutput: { permissionDecision: 'deny' } }, - { policy_action: 'block', hookSpecificOutput: { permissionDecision: 'deny' } }, - { policy_action: 'allow', reason_code: 'harness_not_managed', hookSpecificOutput: { permissionDecision: 'allow' } }, + { policy_action: 'review', reason_code: 'native_command_review_required', hookSpecificOutput: { permissionDecision: 'deny' } }, + { policy_action: 'block', reason_code: 'native_destructive_command', hookSpecificOutput: { permissionDecision: 'deny' } }, + { policy_action: 'allow', hookSpecificOutput: { permissionDecision: 'allow' } }, {}, ]) { assert.equal(decisionFromGuardResponse(response).allow, false); From 8eb9859de24febb14bc21bc56e6c3c12eaccc0a1 Mon Sep 17 00:00:00 2001 From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:15:51 -0400 Subject: [PATCH 7/7] docs: record real Guard availability test --- docs/hol-guard-before-tool.md | 49 +++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/hol-guard-before-tool.md b/docs/hol-guard-before-tool.md index 53d4a2d..ffcbd0e 100644 --- a/docs/hol-guard-before-tool.md +++ b/docs/hol-guard-before-tool.md @@ -21,42 +21,49 @@ HOL Guard already has native pre-tool authority for command decisions. The suppo ## Prepared adapter behavior -The owned fork now contains `scripts/hol-guard-before-tool.mjs`. For `tool_name == "run_command"` it: +The owned fork contains `scripts/hol-guard-before-tool.mjs`. For `tool_name == "run_command"` it: 1. Parses `tool_args` as JSON. 2. Extracts the non-empty `command` string. Missing or malformed command input fails closed. 3. Constructs a Guard `PreToolUse` request with the current project as `cwd`. 4. Sends the complete request through `hol-guard hook --harness codex --json`, with the workspace when available, so Guard's native hook authority remains the decision owner. -5. Allows only a native-rendered `allow` or `warn` result whose hook permission decision is `allow`. -6. Maps review, reapproval, sandbox-required, block, malformed output, unmanaged-harness passthrough, timeout, missing Guard, or any other non-authoritative state to Klaat Code's JSON block contract. +5. Allows an authoritative native `allow` decision, or the explicit `native_policy_warning` allow-with-warning result. +6. Fails closed when native review is unavailable or not ready, including `native_pre_tool_unavailable`, `native_policy_not_ready`, worker/daemon availability failures, unmanaged-harness passthrough, unknown warning results, malformed output, timeout, missing Guard, review, reapproval, sandbox-required, or block. The adapter does not use `hol-guard command explain` as enforcement authority. -## Local preparation proof +## Validation completed -`node --check scripts/hol-guard-before-tool.mjs` passed before the owned-fork write. +The exact owned branch was downloaded into an independent Linux sandbox and validated with Node 20 and Bun 1.4.2. -`node --test scripts/hol-guard-before-tool.test.mjs` passed six focused unit tests covering: +The following checks pass: -- command extraction; -- unrelated hook pass-through; -- malformed `tool_args` fail-closed behavior; -- complete `PreToolUse` envelope construction; -- native allow/warn handling; -- review/block/unmanaged/malformed response rejection. +- `node --check scripts/hol-guard-before-tool.mjs` +- `node --test scripts/hol-guard-before-tool.test.mjs`, 7/7 focused tests +- `bun install --frozen-lockfile` +- `bun run typecheck` +- `bun run build` -A mocked `hol-guard` executable also proved the adapter's process boundary for allow, deny, malformed Klaat input, and unavailable Guard behavior. +The focused tests cover command extraction, unrelated hook pass-through, malformed `tool_args`, complete `PreToolUse` envelope construction, authoritative allow and policy-warning handling, unavailable native review, unknown warnings, review/block responses, and malformed/missing-reason responses. -These are preparation checks only. They are not a substitute for real HOL Guard runtime proof. +## Real HOL Guard 3.4.2 availability test -## Minimum real-runtime validation cases +A clean isolated Python environment installed the published `hol-guard==3.4.2` package successfully. -The implementation is not ready for upstream submission until these cases are exercised against the real Guard runtime: +With no resident native policy authority initialized, `hol-guard hook --harness codex --json` returned `policy_action: warn`, `permissionDecision: allow`, and `reason_code: native_pre_tool_unavailable` for both a benign command and a destructive command. That response means Guard could not complete the native decision. It is not execution proof. + +The first adapter version treated a generic `warn` plus `permissionDecision: allow` as sufficient and therefore would have allowed that unavailable state. The owned-fork adapter now rejects Guard availability and not-ready reason codes and rejects unknown warning results. Re-running the adapter against the same real 3.4.2 runtime now emits Klaat's block result for both test commands while Guard authority is unavailable. + +This closes the unavailable-runtime fail-open defect. It does not yet prove the final authoritative allow/deny path. + +## Remaining real-runtime gate + +The implementation is not ready for upstream submission until an isolated managed Guard runtime can produce authoritative native decisions for these cases: | Case | Expected result | | --- | --- | -| `pwd` | Klaat Code command proceeds | -| `git status --short` | Klaat Code command proceeds | +| `pwd` | Klaat Code command proceeds after authoritative Guard allow | +| `git status --short` | Klaat Code command proceeds after authoritative Guard allow | | `rm -rf /` | Klaat Code command is blocked | | `shred ~/.ssh/id_ed25519` | Klaat Code command is blocked | | malformed `tool_args` | fail closed | @@ -64,6 +71,8 @@ The implementation is not ready for upstream submission until these cases are ex The deny-path test must prove that the target command is not executed. +An isolated `hol-guard install codex` attempt in the validation sandbox did not establish that authority because Guard rejected the sandbox interpreter as writable by another user. No real user installation was modified. A clean authoritative setup remains required before upstream submission. + ## Candidate hook configuration The adapter should remain project-local until there is a stable shipped integration surface: @@ -87,8 +96,8 @@ Before any upstream PR: - revalidate the current Klaat Code upstream head, hook payload, block semantics, contribution guidance, and queue ownership; - revalidate HOL Guard's current native hook entry point; -- run Klaat Code typecheck/build plus the focused adapter tests; -- prove one allow path and one deny path with the real Guard runtime, including target command non-execution on deny; +- keep Klaat Code typecheck/build plus focused adapter tests green; +- prove one authoritative allow path and one authoritative deny path with the real Guard runtime, including target command non-execution on deny; - confirm the contribution still fits current Distribution WIP and maintainer-conversion gates. Until those gates pass, this remains owned-fork preparation and carries no strict outcome value.