From 0f27c79b63922a7cb042eb65a8ec21d44fbc6805 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Fri, 31 Jul 2026 16:07:45 -0700 Subject: [PATCH 01/10] fix(agentic-ai): deploy hooks for Codex --- agentic-ai/Claude/install.sh | 16 +++++++++------- agentic-ai/Claude/validate.sh | 17 ++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/agentic-ai/Claude/install.sh b/agentic-ai/Claude/install.sh index e91af3b..49fb895 100755 --- a/agentic-ai/Claude/install.sh +++ b/agentic-ai/Claude/install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Idempotent setup: deploys this repo's Claude config into ~/.claude/ -# (settings.json is copied, everything else symlinked). +# Idempotent setup: deploys this repo's Claude config and shared Codex hooks. +# Claude settings are copied; instructions, docs, policies, and hooks are symlinked. # Safe to re-run. Backs up any existing settings.json before replacing it. set -euo pipefail @@ -8,7 +8,7 @@ set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGENTIC_DIR="$(cd "$REPO_DIR/.." && pwd)" CLAUDE_DIR="$HOME/.claude" -HOOKS_DIR="$CLAUDE_DIR/hooks" +HOOKS_DIRS=("$CLAUDE_DIR/hooks" "$HOME/.codex/hooks") SETTINGS="$CLAUDE_DIR/settings.json" if [[ -d "$CLAUDE_DIR/docs" && ! -L "$CLAUDE_DIR/docs" ]]; then @@ -67,14 +67,16 @@ printf 'Linked: docs/\n' ln -sf "$REPO_DIR/railguard.yaml" "$HOME/.railguard.yaml" printf 'Linked: railguard.yaml → ~/.railguard.yaml\n' -# Create hooks dir if it doesn't exist -mkdir -p "$HOOKS_DIR" +# Create hook dirs if they don't exist +mkdir -p "${HOOKS_DIRS[@]}" # Symlink each hook script and ensure it's executable for hook in "$REPO_DIR/hooks/"*.sh; do chmod +x "$hook" - ln -sf "$hook" "$HOOKS_DIR/$(basename "$hook")" - printf 'Linked: hooks/%s\n' "$(basename "$hook")" + for hooks_dir in "${HOOKS_DIRS[@]}"; do + ln -sf "$hook" "$hooks_dir/$(basename "$hook")" + printf 'Linked: %s/%s\n' "$hooks_dir" "$(basename "$hook")" + done done # Install (or migrate) the railguard binary from the GitHub source. diff --git a/agentic-ai/Claude/validate.sh b/agentic-ai/Claude/validate.sh index 87eb9d3..57adefc 100755 --- a/agentic-ai/Claude/validate.sh +++ b/agentic-ai/Claude/validate.sh @@ -8,7 +8,7 @@ set -uo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGENTIC_DIR="$(cd "$REPO_DIR/.." && pwd)" CLAUDE_DIR="$HOME/.claude" -HOOKS_DIR="$CLAUDE_DIR/hooks" +HOOKS_DIRS=("$CLAUDE_DIR/hooks" "$HOME/.codex/hooks") ERRORS=0 pass() { printf ' [ OK ] %s\n' "$1"; } @@ -57,20 +57,19 @@ done section "Hooks" for hook in "$REPO_DIR/hooks/"*.sh; do name="$(basename "$hook")" - linked="$HOOKS_DIR/$name" - if ! [[ -L "$linked" && "$(readlink "$linked")" == "$hook" ]]; then - fail "hooks/$name: not linked in $HOOKS_DIR" - continue - fi if ! [[ -x "$hook" ]]; then fail "hooks/$name: not executable (run: chmod +x $hook)" - continue + else + pass "hooks/$name: source is executable" fi if ! bash -n "$hook" 2>/dev/null; then fail "hooks/$name: bash syntax error" - continue + else + pass "hooks/$name: source syntax" fi - pass "hooks/$name" + for hooks_dir in "${HOOKS_DIRS[@]}"; do + check_symlink "$hooks_dir/$name" "$hook" + done done # ── Required binaries ───────────────────────────────────────────────────────── From b8a8d65002f43b3319632215271715a4a2c38413 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Fri, 31 Jul 2026 16:07:45 -0700 Subject: [PATCH 02/10] docs(agentic-ai): document Codex hook deployment --- agentic-ai/Claude/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/agentic-ai/Claude/README.md b/agentic-ai/Claude/README.md index 00410ea..6ff5f42 100644 --- a/agentic-ai/Claude/README.md +++ b/agentic-ai/Claude/README.md @@ -1,8 +1,8 @@ # Claude Code Config Version-controlled source of truth for `~/.claude/` settings, hooks, rules, and -on-demand docs. Running `install.sh` copies the settings template and links the -rest of the configuration. +on-demand docs, plus the hook scripts shared with Codex. Running `install.sh` +copies the Claude settings template and links the shared configuration. ## Activation @@ -19,18 +19,24 @@ This will: `~/.claude/`, `~/.codex/`, and `~/` (the shared cross-agent set) - Symlink `~/.claude/docs/` → this `docs/` - Symlink this `railguard.yaml` → `~/.railguard.yaml` -- Symlink each `hooks/*.sh` script into `~/.claude/hooks/` +- Symlink each `hooks/*.sh` script into both `~/.claude/hooks/` and + `~/.codex/hooks/` - Install or update the Railguard fork with `cargo install --git https://github.com/ulises-c/railguard` (requires Rust/cargo; an existing binary is kept with a warning when cargo is unavailable) - Run `railguard install` to register it as a global PreToolUse hook -Restart Claude Code after running. +Restart both Claude Code and Codex after running so each reloads its hook +configuration. > **Note:** `settings.json` sets `bypassPermissions` at the user level, so it applies to **all projects**, not just this repo. ## What this configures +The hook scripts below are deployed for both Claude Code and Codex. Claude hook +registration lives in `settings.json`; Codex hook registration remains in +`~/.codex/hooks.json`, which this installer does not rewrite. + ### `bypassPermissions` Claude auto-approves all tool calls without prompting. The hooks below act as the safety gate. From 22156d9fc5ba08be57e0c4dddc5a806911d6e0a5 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Fri, 31 Jul 2026 17:10:16 -0700 Subject: [PATCH 03/10] fix(agentic-ai): register and handle Codex hooks --- .../Claude/hooks/post-edit-shellcheck.sh | 37 ++++++++++---- agentic-ai/Claude/hooks/post-test-runner.sh | 32 +++++++++--- agentic-ai/Claude/hooks/validate-bash.sh | 5 +- agentic-ai/Claude/hooks/validate-write.sh | 33 ++++++++---- agentic-ai/Claude/install.sh | 51 ++++++++++++++++++- agentic-ai/Claude/validate.sh | 48 +++++++++++++++-- 6 files changed, 175 insertions(+), 31 deletions(-) diff --git a/agentic-ai/Claude/hooks/post-edit-shellcheck.sh b/agentic-ai/Claude/hooks/post-edit-shellcheck.sh index 39c93f1..dc1ffbd 100755 --- a/agentic-ai/Claude/hooks/post-edit-shellcheck.sh +++ b/agentic-ai/Claude/hooks/post-edit-shellcheck.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# PostToolUse hook for Write/Edit/MultiEdit: runs shellcheck on edited .sh files. +# PostToolUse hook for Write/Edit/MultiEdit/apply_patch: runs shellcheck on edited .sh files. # Exit 2 = block (Claude sees stderr and must fix before continuing). set -euo pipefail trap 'exit 2' ERR @@ -7,15 +7,32 @@ trap 'exit 2' ERR command -v shellcheck &>/dev/null || exit 0 INPUT=$(cat) -FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""') +CWD=$(jq -r '.cwd // ""' <<< "$INPUT") +FILES=$(jq -r ' + if (.tool_input.file_path // "") != "" then + .tool_input.file_path + else + (.tool_input.command // "") + | split("\n")[] + | select(test("^\\*\\*\\* (Add|Update|Delete) File: |^\\*\\*\\* Move to: ")) + | sub("^\\*\\*\\* (Add|Update|Delete) File: "; "") + | sub("^\\*\\*\\* Move to: "; "") + end +' <<< "$INPUT") -[[ "$FILE" == *.sh ]] || exit 0 -[[ -f "$FILE" ]] || exit 0 +while IFS= read -r file; do + [[ -n "$file" ]] || continue + if [[ "$file" != /* && -n "$CWD" ]]; then + file="$CWD/$file" + fi + [[ "$file" == *.sh ]] || continue + [[ -f "$file" ]] || continue -# Skip zsh shebangs — only sh/bash/dash/ksh/busybox are supported (SC1071). -[[ "$(head -n1 "$FILE")" == *zsh* ]] && exit 0 + # Skip zsh shebangs — only sh/bash/dash/ksh/busybox are supported (SC1071). + [[ "$(head -n1 "$file")" == *zsh* ]] && continue -if ! shellcheck --severity=error "$FILE" >&2; then - printf '\npost-edit-shellcheck.sh: shellcheck errors in %s — fix before continuing.\n' "$FILE" >&2 - exit 2 -fi + if ! shellcheck --severity=error "$file" >&2; then + printf '\npost-edit-shellcheck.sh: shellcheck errors in %s — fix before continuing.\n' "$file" >&2 + exit 2 + fi +done <<< "$FILES" diff --git a/agentic-ai/Claude/hooks/post-test-runner.sh b/agentic-ai/Claude/hooks/post-test-runner.sh index b15fdb1..fad7be3 100755 --- a/agentic-ai/Claude/hooks/post-test-runner.sh +++ b/agentic-ai/Claude/hooks/post-test-runner.sh @@ -1,17 +1,35 @@ #!/usr/bin/env bash -# PostToolUse hook for Write/Edit/MultiEdit: runs the project test suite after source edits. +# PostToolUse hook for Write/Edit/MultiEdit/apply_patch: runs the project test suite after source edits. # Exit 2 = Claude sees failure output (warn). Exit 0 = passed (timing shown) or no suite found. set -uo pipefail INPUT=$(cat) || exit 0 -FILE=$(jq -r '.tool_input.file_path // ""' <<< "$INPUT" 2>/dev/null) || exit 0 +CWD=$(jq -r '.cwd // ""' <<< "$INPUT" 2>/dev/null) || exit 0 +FILES=$(jq -r ' + if (.tool_input.file_path // "") != "" then + .tool_input.file_path + else + (.tool_input.command // "") + | split("\n")[] + | select(test("^\\*\\*\\* (Add|Update|Delete) File: |^\\*\\*\\* Move to: ")) + | sub("^\\*\\*\\* (Add|Update|Delete) File: "; "") + | sub("^\\*\\*\\* Move to: "; "") + end +' <<< "$INPUT" 2>/dev/null) || exit 0 +FILE="" +while IFS= read -r candidate; do + [[ -n "$candidate" ]] || continue + if [[ "$candidate" != /* && -n "$CWD" ]]; then + candidate="$CWD/$candidate" + fi + case "${candidate##*.}" in + md|txt|json|yaml|yml|toml|lock|rst|svg|png|jpg|jpeg|gif|pdf|ico) continue ;; + esac + FILE="$candidate" + break +done <<< "$FILES" [[ -n "$FILE" ]] || exit 0 -# Skip non-source extensions -case "${FILE##*.}" in - md|txt|json|yaml|yml|toml|lock|rst|svg|png|jpg|jpeg|gif|pdf|ico) exit 0 ;; -esac - # Resolve project root via git; no repo = no test suite ROOT=$(git -C "$(dirname "$FILE")" rev-parse --show-toplevel 2>/dev/null) || exit 0 diff --git a/agentic-ai/Claude/hooks/validate-bash.sh b/agentic-ai/Claude/hooks/validate-bash.sh index e406c9b..59befce 100755 --- a/agentic-ai/Claude/hooks/validate-bash.sh +++ b/agentic-ai/Claude/hooks/validate-bash.sh @@ -4,7 +4,10 @@ set -euo pipefail trap 'exit 2' ERR -COMMAND=$(jq -r '.tool_input.command // ""') +INPUT=$(cat) +TOOL_NAME=$(jq -r '.tool_name // ""' <<< "$INPUT") +COMMAND=$(jq -r '.tool_input.command // ""' <<< "$INPUT") +[[ "$TOOL_NAME" == "apply_patch" || "$TOOL_NAME" == "functions.apply_patch" ]] && exit 0 # Heredoc bodies are message text, not executed code — policy checks use only the first line. FIRST_LINE=$(head -1 <<< "$COMMAND") diff --git a/agentic-ai/Claude/hooks/validate-write.sh b/agentic-ai/Claude/hooks/validate-write.sh index 96d3c63..9a0fa37 100755 --- a/agentic-ai/Claude/hooks/validate-write.sh +++ b/agentic-ai/Claude/hooks/validate-write.sh @@ -1,20 +1,28 @@ #!/usr/bin/env bash -# PreToolUse hook for Write/Edit/MultiEdit: blocks writes to sensitive file paths. +# PreToolUse hook for Write/Edit/MultiEdit/apply_patch: blocks writes to sensitive file paths. # Exit 2 = block the tool call (stderr is shown to Claude as the reason). set -euo pipefail trap 'exit 2' ERR INPUT=$(cat) -FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""') +CWD=$(jq -r '.cwd // ""' <<< "$INPUT") +FILE_PATHS=$(jq -r ' + if (.tool_input.file_path // "") != "" then + .tool_input.file_path + else + (.tool_input.command // "") + | split("\n")[] + | select(test("^\\*\\*\\* (Add|Update|Delete) File: |^\\*\\*\\* Move to: ")) + | sub("^\\*\\*\\* (Add|Update|Delete) File: "; "") + | sub("^\\*\\*\\* Move to: "; "") + end +' <<< "$INPUT") block() { printf 'validate-write.sh blocked: %s\n' "$1" >&2 exit 2 } -# Expand leading ~ to $HOME for comparison -EXPANDED="${FILE_PATH/#\~/$HOME}" - SENSITIVE_PREFIXES=( "$HOME/.ssh" "$HOME/.aws" @@ -27,10 +35,17 @@ SENSITIVE_PREFIXES=( "/proc" ) -for prefix in "${SENSITIVE_PREFIXES[@]}"; do - if [[ "$EXPANDED" == "$prefix"* ]]; then - block "write to sensitive path: $FILE_PATH" +while IFS= read -r file_path; do + [[ -n "$file_path" ]] || continue + expanded="${file_path/#\~/$HOME}" + if [[ "$expanded" != /* && -n "$CWD" ]]; then + expanded="$CWD/$expanded" fi -done + for prefix in "${SENSITIVE_PREFIXES[@]}"; do + if [[ "$expanded" == "$prefix" || "$expanded" == "$prefix/"* ]]; then + block "write to sensitive path: $file_path" + fi + done +done <<< "$FILE_PATHS" exit 0 diff --git a/agentic-ai/Claude/install.sh b/agentic-ai/Claude/install.sh index 49fb895..3f7d66d 100755 --- a/agentic-ai/Claude/install.sh +++ b/agentic-ai/Claude/install.sh @@ -112,6 +112,55 @@ fi # absolute paths; redeploy the template afterwards so portable ~ paths win. printf 'Configuring railguard...\n' "$RAILGUARD_BIN" install + +CODEX_HOOKS="$HOME/.codex/hooks.json" +CODEX_HOOKS_TMP=$(mktemp "$HOME/.codex/hooks.json.tmp.XXXXXX") +trap 'rm -f "$CODEX_HOOKS_TMP"' EXIT +jq \ + --arg managed '/(validate-bash|validate-write|post-edit-shellcheck|post-test-runner|driftcheck)\.sh' \ + --arg validate_bash "bash \"$HOME/.codex/hooks/validate-bash.sh\"" \ + --arg validate_write "bash \"$HOME/.codex/hooks/validate-write.sh\"" \ + --arg post_shellcheck "bash \"$HOME/.codex/hooks/post-edit-shellcheck.sh\"" \ + --arg post_test "bash \"$HOME/.codex/hooks/post-test-runner.sh\"" \ + --arg driftcheck "bash \"$HOME/.codex/hooks/driftcheck.sh\"" ' + def strip_managed: + map( + .hooks = [ + (.hooks // [])[] + | select((((.command // "") | test($managed))) | not) + ] + ) + | map(select((.hooks | length) > 0)); + .hooks //= {} | + .hooks.PreToolUse = ((.hooks.PreToolUse // [] | strip_managed) + [{ + "matcher": "", + "hooks": [ + {"type": "command", "command": $validate_bash, "timeout": 5}, + {"type": "command", "command": $validate_write, "timeout": 5} + ] + }]) | + .hooks.PostToolUse = ((.hooks.PostToolUse // [] | strip_managed) + [{ + "matcher": "", + "hooks": [ + {"type": "command", "command": $post_shellcheck, "timeout": 5}, + {"type": "command", "command": $post_test, "timeout": 75} + ] + }]) | + .hooks.Stop = ((.hooks.Stop // [] | strip_managed) + [{ + "matcher": "", + "hooks": [{"type": "command", "command": $driftcheck, "timeout": 5}] + }]) +' "$CODEX_HOOKS" > "$CODEX_HOOKS_TMP" +if cmp -s "$CODEX_HOOKS" "$CODEX_HOOKS_TMP"; then + rm -f "$CODEX_HOOKS_TMP" + printf 'Codex custom hooks already registered\n' +else + CODEX_HOOKS_BACKUP="$CODEX_HOOKS.bak.$(date +%Y%m%d%H%M%S)" + cp "$CODEX_HOOKS" "$CODEX_HOOKS_BACKUP" + mv "$CODEX_HOOKS_TMP" "$CODEX_HOOKS" + printf 'Registered Codex custom hooks (backup: %s)\n' "$CODEX_HOOKS_BACKUP" +fi +trap - EXIT cp "$REPO_DIR/settings.json" "$SETTINGS" # Warn on Ubuntu 24.04+ if the bwrap AppArmor profile isn't set up @@ -121,4 +170,4 @@ if grep -qi 'ubuntu' /etc/os-release 2>/dev/null && ! [[ -f /etc/apparmor.d/bwra printf ' bash %s/setup-linux-sandbox.sh\n' "$REPO_DIR" fi -printf '\nDone. Restart Claude Code for changes to take effect.\n' +printf '\nDone. Restart Claude Code and Codex for changes to take effect.\n' diff --git a/agentic-ai/Claude/validate.sh b/agentic-ai/Claude/validate.sh index 57adefc..6e25f38 100755 --- a/agentic-ai/Claude/validate.sh +++ b/agentic-ai/Claude/validate.sh @@ -72,6 +72,30 @@ for hook in "$REPO_DIR/hooks/"*.sh; do done done +section "Codex hook registration" +CODEX_HOOKS="$HOME/.codex/hooks.json" +if ! jq empty "$CODEX_HOOKS" >/dev/null 2>&1; then + fail "$CODEX_HOOKS: missing or invalid JSON" +else + for registration in \ + "PreToolUse:validate-bash.sh" \ + "PreToolUse:validate-write.sh" \ + "PostToolUse:post-edit-shellcheck.sh" \ + "PostToolUse:post-test-runner.sh" \ + "Stop:driftcheck.sh"; do + event="${registration%%:*}" + name="${registration#*:}" + if jq -e --arg event "$event" --arg name "$name" ' + [.hooks[$event][]?.hooks[]?.command // empty | select(contains("/" + $name))] + | length > 0 + ' "$CODEX_HOOKS" >/dev/null 2>&1; then + pass "Codex $event hook registered: $name" + else + fail "Codex $event hook missing: $name" + fi + done +fi + # ── Required binaries ───────────────────────────────────────────────────────── section "Required binaries" check_bin() { @@ -161,6 +185,18 @@ else fail "validate-bash.sh: incorrectly blocked a safe command" fi +if run_hook validate-bash.sh '{"tool_name":"apply_patch","tool_input":{"command":"*** Begin Patch\n*** Add File: script.sh\n+sudo --version\n*** End Patch"}}'; then + pass "validate-bash.sh: ignores Codex patch contents" +else + fail "validate-bash.sh: scanned Codex patch contents as shell commands" +fi + +if run_hook validate-bash.sh '{"tool_name":"Bash","tool_input":{"command":"sudo --version"}}'; then + fail "validate-bash.sh: trusted a sudo Bash command" +else + pass "validate-bash.sh: still blocks sudo Bash commands" +fi + # validate-write.sh: must block writes to sensitive paths if run_hook validate-write.sh '{"tool_input":{"file_path":"/etc/passwd"}}'; then fail "validate-write.sh: did not block write to /etc/passwd" @@ -181,6 +217,12 @@ else fail "validate-write.sh: incorrectly blocked /tmp/test.txt" fi +if run_hook validate-write.sh '{"cwd":"/tmp","tool_input":{"command":"*** Begin Patch\n*** Update File: /etc/passwd\n@@\n-old\n+new\n*** End Patch"}}'; then + fail "validate-write.sh: did not block a sensitive Codex patch" +else + pass "validate-write.sh: blocks sensitive Codex patches" +fi + # post-edit-shellcheck.sh: must pass on a valid script if printf '{"tool_input":{"file_path":"%s"}}' "$REPO_DIR/hooks/validate-bash.sh" \ | bash "$REPO_DIR/hooks/post-edit-shellcheck.sh" &>/dev/null; then @@ -193,11 +235,11 @@ fi _TMPSH=$(mktemp /tmp/bad-XXXXXX.sh) trap 'rm -f "$_TMPSH"' EXIT printf '#!/usr/bin/env bash\nFOO=$(\n' > "$_TMPSH" -if printf '{"tool_input":{"file_path":"%s"}}' "$_TMPSH" \ +if printf '{"cwd":"/tmp","tool_input":{"command":"*** Begin Patch\\n*** Update File: %s\\n@@\\n-old\\n+new\\n*** End Patch"}}' "$_TMPSH" \ | bash "$REPO_DIR/hooks/post-edit-shellcheck.sh" &>/dev/null; then - fail "post-edit-shellcheck.sh: failed to catch a syntax error" + fail "post-edit-shellcheck.sh: failed to catch a Codex patch syntax error" else - pass "post-edit-shellcheck.sh: catches shell syntax errors" + pass "post-edit-shellcheck.sh: catches shell syntax errors from Codex patches" fi rm -f "$_TMPSH"; trap - EXIT From d2f79f81c919febc373f157be7d66420efd24671 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Fri, 31 Jul 2026 17:10:40 -0700 Subject: [PATCH 04/10] fix(setup): keep pipx dry-runs side-effect free --- lib/core.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core.sh b/lib/core.sh index 3a8d101..7970699 100755 --- a/lib/core.sh +++ b/lib/core.sh @@ -819,8 +819,8 @@ ghostty_deploy_linux() { desktop_pipx_section() { printf '\n==> Installing pipx packages...\n' - if command -v pipx &>/dev/null; then - pipx ensurepath + if [[ "$DRY_RUN" == true ]] || command -v pipx &>/dev/null; then + [[ "$DRY_RUN" == false ]] && pipx ensurepath pipx_install_tier "medium" else printf ' pipx not found — skipping\n' From 8798c7ef6886c802eb59bd27a35723871256c1fd Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Fri, 31 Jul 2026 17:11:05 -0700 Subject: [PATCH 05/10] test(agentic-ai): benchmark Codex hook behavior --- agentic-ai/Claude/README.md | 32 ++- agentic-ai/Claude/benchmark-codex-hooks.sh | 255 +++++++++++++++++++++ docs/CHANGELOG.md | 19 ++ 3 files changed, 301 insertions(+), 5 deletions(-) create mode 100755 agentic-ai/Claude/benchmark-codex-hooks.sh diff --git a/agentic-ai/Claude/README.md b/agentic-ai/Claude/README.md index 6ff5f42..170583b 100644 --- a/agentic-ai/Claude/README.md +++ b/agentic-ai/Claude/README.md @@ -21,6 +21,8 @@ This will: - Symlink this `railguard.yaml` → `~/.railguard.yaml` - Symlink each `hooks/*.sh` script into both `~/.claude/hooks/` and `~/.codex/hooks/` +- Merge the custom hook registrations into `~/.codex/hooks.json` while + preserving unrelated hooks - Install or update the Railguard fork with `cargo install --git https://github.com/ulises-c/railguard` (requires Rust/cargo; an existing binary is kept with a warning when cargo is unavailable) @@ -34,8 +36,9 @@ configuration. ## What this configures The hook scripts below are deployed for both Claude Code and Codex. Claude hook -registration lives in `settings.json`; Codex hook registration remains in -`~/.codex/hooks.json`, which this installer does not rewrite. +registration lives in `settings.json`; the installer idempotently merges the +custom registrations into `~/.codex/hooks.json` after Railguard registers its +own hooks, preserving unrelated entries. ### `bypassPermissions` Claude auto-approves all tool calls without prompting. The hooks below act as the safety gate. @@ -67,17 +70,17 @@ Blocks dangerous or escalation-prone shell commands: - `sudo` (escalation must be explicit — run yourself) - `git add -A`, `git add --all`, `git add .` (bulk staging can silently include secrets) -### PreToolUse: `validate-write.sh` (Write / Edit / MultiEdit) +### PreToolUse: `validate-write.sh` (Write / Edit / MultiEdit / apply_patch) Blocks writes to sensitive file paths: - `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `~/.config/gh` - `/etc/`, `/usr/`, `/boot/`, `/sys/`, `/proc/` -### PostToolUse: `post-edit-shellcheck.sh` (Write / Edit / MultiEdit) +### PostToolUse: `post-edit-shellcheck.sh` (Write / Edit / MultiEdit / apply_patch) After any shell script edit, runs `shellcheck --severity=error`. Exits 2 if errors are found, forcing Claude to fix them before continuing. Skips gracefully if `shellcheck` is not installed. -### PostToolUse: `post-test-runner.sh` (Write / Edit / MultiEdit) +### PostToolUse: `post-test-runner.sh` (Write / Edit / MultiEdit / apply_patch) After any source file edit, auto-detects and runs the project test suite. Detection order: `.claude/test-cmd` override → `Cargo.toml` → `go.mod` → `pyproject.toml`/`pytest.ini` → `package.json` → `Makefile`. Skips non-source extensions (md, json, yaml, etc.) and projects with no recognized test suite. @@ -143,6 +146,25 @@ beside it. This is why the pre-existing untracked `~/AGENTS.md` was inert: its ## Testing the hooks +Run the repeatable Codex benchmark first. It exercises fixed Railguard and +custom-hook protocol cases in a disposable Git repository, emits TAP with +per-case timings, and exits nonzero on any regression. It does not execute the +dangerous commands in its fixtures or modify live Codex configuration. + +```bash +# Benchmark the installed binary +bash agentic-ai/Claude/benchmark-codex-hooks.sh + +# Run the identical cases against a development build +RAILGUARD_BIN=/path/to/railguard/target/debug/railguard \ + bash agentic-ai/Claude/benchmark-codex-hooks.sh + +# Validate deployed hook links and registrations separately +bash agentic-ai/Claude/validate.sh +``` + +The commands below remain useful for quick, individual hook probes: + ```bash # Should exit 2 (blocked) echo '{"tool_input":{"command":"rm -rf /"}}' | bash agentic-ai/Claude/hooks/validate-bash.sh diff --git a/agentic-ai/Claude/benchmark-codex-hooks.sh b/agentic-ai/Claude/benchmark-codex-hooks.sh new file mode 100755 index 0000000..5e18157 --- /dev/null +++ b/agentic-ai/Claude/benchmark-codex-hooks.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +# Deterministic Codex hook benchmark. Runs fixed protocol cases in disposable state. +set -uo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +HOOKS_DIR="$SCRIPT_DIR/hooks" +RAILGUARD_BIN=${RAILGUARD_BIN:-railguard} +CASE_INDEX=0 +FAILURES=0 +LAST_STATUS=0 +LAST_OUTPUT="" +LAST_ERROR="" +LAST_DETAIL="" + +resolve_railguard() { + if [[ "$RAILGUARD_BIN" == */* ]]; then + [[ -x "$RAILGUARD_BIN" ]] || return 1 + else + RAILGUARD_BIN=$(command -v "$RAILGUARD_BIN") || return 1 + fi +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || { + printf 'Bail out! required command is unavailable: %s\n' "$1" + exit 2 + } +} + +resolve_railguard || { + printf 'Bail out! Railguard binary is unavailable: %s\n' "$RAILGUARD_BIN" + exit 2 +} +require_command bash +require_command git +require_command jq +require_command shellcheck + +BENCH_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/codex-hook-benchmark.XXXXXX") +PROJECT_DIR="$BENCH_ROOT/project" +OUTSIDE_DIR="$BENCH_ROOT/outside" +RAILGUARD_STATE="$BENCH_ROOT/railguard-state" + +cleanup() { + rm -rf -- "$BENCH_ROOT" +} +trap cleanup EXIT + +mkdir -p "$PROJECT_DIR/.claude" "$PROJECT_DIR/src" "$OUTSIDE_DIR" "$RAILGUARD_STATE" +git init --quiet "$PROJECT_DIR" +printf 'true\n' > "$PROJECT_DIR/.claude/test-cmd" +printf '#!/usr/bin/env bash\nprintf "benchmark\\n"\n' > "$PROJECT_DIR/valid.sh" +printf '#!/usr/bin/env bash\nlocal value=1\nprintf "%%s\\n" "$value"\n' > "$PROJECT_DIR/invalid.sh" +printf 'pub fn benchmark_probe() {}\n' > "$PROJECT_DIR/src/probe.rs" +printf 'version: 1\nblocklist: []\nfence:\n enabled: true\n allowed_paths:\n - "%s"\n' \ + "$PROJECT_DIR" > "$PROJECT_DIR/railguard.yaml" + +now_ms() { + local ms + ms=$(date +%s%3N 2>/dev/null) + if [[ -z "$ms" || "$ms" == *N* ]]; then + if command -v python3 >/dev/null 2>&1; then + ms=$(python3 -c 'import time; print(int(time.time() * 1000))') + else + ms=$(( $(date +%s) * 1000 )) + fi + fi + printf '%s\n' "$ms" +} + +fail_case() { + LAST_DETAIL=$1 + return 1 +} + +capture_hook() { + local hook=$1 + local input=$2 + local stdout_file="$BENCH_ROOT/hook.stdout" + local stderr_file="$BENCH_ROOT/hook.stderr" + + printf '%s\n' "$input" | bash "$hook" > "$stdout_file" 2> "$stderr_file" + LAST_STATUS=$? + LAST_OUTPUT=$(< "$stdout_file") + LAST_ERROR=$(< "$stderr_file") +} + +capture_railguard() { + local tool_name=$1 + local tool_input=$2 + local stdout_file="$BENCH_ROOT/railguard.stdout" + local stderr_file="$BENCH_ROOT/railguard.stderr" + local input + + input=$(jq -nc \ + --arg session_id "codex-hook-benchmark-$$-$CASE_INDEX" \ + --arg cwd "$PROJECT_DIR" \ + --arg tool_name "$tool_name" \ + --argjson tool_input "$tool_input" \ + '{session_id: $session_id, cwd: $cwd, hook_event_name: "PreToolUse", tool_name: $tool_name, tool_input: $tool_input, tool_use_id: "benchmark"}') + + printf '%s\n' "$input" | env \ + RAILGUARD_HOME="$RAILGUARD_STATE" \ + RAILGUARD_NO_KILL=1 \ + "$RAILGUARD_BIN" hook --client codex --event PreToolUse \ + > "$stdout_file" 2> "$stderr_file" + LAST_STATUS=$? + LAST_OUTPUT=$(< "$stdout_file") + LAST_ERROR=$(< "$stderr_file") +} + +expect_status() { + local expected=$1 + local context=$2 + [[ $LAST_STATUS -eq $expected ]] || fail_case "$context: expected exit $expected, got $LAST_STATUS" +} + +expect_output() { + local filter=$1 + local context=$2 + jq -e "$filter" <<< "$LAST_OUTPUT" >/dev/null 2>&1 \ + || fail_case "$context: unexpected output: $LAST_OUTPUT" +} + +case_railguard_safe_noop() { + capture_railguard Bash "$(jq -nc '{command: "git status --short"}')" + expect_status 0 "Railguard safe response" || return 1 + expect_output 'type == "object" and length == 0' "Codex safe response must omit permissionDecision" +} + +case_railguard_hard_deny() { + capture_railguard Bash "$(jq -nc '{command: "rm -rf /"}')" + expect_status 0 "Railguard hard deny" || return 1 + expect_output '.hookSpecificOutput.permissionDecision == "deny"' "destructive command must be denied" +} + +case_railguard_approval_becomes_deny() { + capture_railguard Bash "$(jq -nc '{command: "npm publish"}')" + expect_status 0 "Railguard approval response" || return 1 + expect_output \ + '.hookSpecificOutput.permissionDecision == "deny" and (.hookSpecificOutput.permissionDecisionReason | ascii_downcase | contains("requires human approval"))' \ + "Codex approval-required response must be an actionable deny" +} + +case_railguard_outside_fence() { + capture_railguard Write "$(jq -nc --arg path "$OUTSIDE_DIR/probe.txt" '{file_path: $path, content: "probe"}')" + expect_status 0 "Railguard path fence response" || return 1 + expect_output '.hookSpecificOutput.permissionDecision == "deny"' "write outside project fence must be denied" +} + +case_validate_bash_safe() { + capture_hook "$HOOKS_DIR/validate-bash.sh" \ + "$(jq -nc '{tool_name: "Bash", tool_input: {command: "git status --short"}}')" + expect_status 0 "safe custom Bash hook" +} + +case_validate_bash_bulk_stage() { + capture_hook "$HOOKS_DIR/validate-bash.sh" \ + "$(jq -nc '{tool_name: "Bash", tool_input: {command: "git add --all"}}')" + expect_status 2 "bulk staging custom Bash hook" +} + +case_validate_bash_patch_text() { + capture_hook "$HOOKS_DIR/validate-bash.sh" \ + "$(jq -nc --arg cwd "$PROJECT_DIR" --arg command $'*** Begin Patch\n*** Update File: README.md\n@@\n+sudo --version\n*** End Patch' '{cwd: $cwd, tool_name: "apply_patch", tool_input: {command: $command}}')" + expect_status 0 "apply_patch text must not be parsed as a shell command" +} + +case_validate_write_sensitive_patch() { + capture_hook "$HOOKS_DIR/validate-write.sh" \ + "$(jq -nc --arg cwd "$PROJECT_DIR" --arg command $'*** Begin Patch\n*** Update File: /etc/benchmark-probe\n@@\n+probe\n*** End Patch' '{cwd: $cwd, tool_name: "apply_patch", tool_input: {command: $command}}')" + expect_status 2 "sensitive Codex patch path" +} + +case_shellcheck_valid_patch() { + capture_hook "$HOOKS_DIR/post-edit-shellcheck.sh" \ + "$(jq -nc --arg cwd "$PROJECT_DIR" --arg command $'*** Begin Patch\n*** Update File: valid.sh\n*** End Patch' '{cwd: $cwd, tool_name: "apply_patch", tool_input: {command: $command}}')" + expect_status 0 "valid shell patch" +} + +case_shellcheck_invalid_patch() { + capture_hook "$HOOKS_DIR/post-edit-shellcheck.sh" \ + "$(jq -nc --arg cwd "$PROJECT_DIR" --arg command $'*** Begin Patch\n*** Update File: invalid.sh\n*** End Patch' '{cwd: $cwd, tool_name: "apply_patch", tool_input: {command: $command}}')" + expect_status 2 "invalid shell patch" +} + +case_test_runner_patch() { + capture_hook "$HOOKS_DIR/post-test-runner.sh" \ + "$(jq -nc --arg cwd "$PROJECT_DIR" --arg command $'*** Begin Patch\n*** Update File: src/probe.rs\n*** End Patch' '{cwd: $cwd, tool_name: "apply_patch", tool_input: {command: $command}}')" + expect_status 0 "Codex source patch test runner" || return 1 + [[ "$LAST_ERROR" == *'post-test-runner: true passed'* ]] \ + || fail_case "test runner did not report the isolated test command: $LAST_ERROR" +} + +run_case() { + local name=$1 + local function_name=$2 + local start_ms end_ms elapsed_ms + + CASE_INDEX=$(( CASE_INDEX + 1 )) + LAST_DETAIL="" + start_ms=$(now_ms) + if "$function_name"; then + end_ms=$(now_ms) + elapsed_ms=$(( end_ms - start_ms )) + printf 'ok %d - %s # time=%dms\n' "$CASE_INDEX" "$name" "$elapsed_ms" + else + end_ms=$(now_ms) + elapsed_ms=$(( end_ms - start_ms )) + FAILURES=$(( FAILURES + 1 )) + printf 'not ok %d - %s # time=%dms\n' "$CASE_INDEX" "$name" "$elapsed_ms" + printf '# %s\n' "${LAST_DETAIL:-case failed without diagnostics}" + fi +} + +CASE_NAMES=( + 'Railguard safe Codex response is a no-op' + 'Railguard hard block is a deny' + 'Railguard approval requirement becomes a Codex deny' + 'Railguard rejects a write outside the project fence' + 'custom Bash hook allows a safe command' + 'custom Bash hook blocks bulk staging' + 'custom Bash hook ignores apply_patch body text' + 'custom write hook blocks a sensitive patch path' + 'post-edit ShellCheck accepts a valid patch' + 'post-edit ShellCheck catches an invalid patch' + 'post-edit test runner executes for a Codex source patch' +) +CASE_FUNCTIONS=( + case_railguard_safe_noop + case_railguard_hard_deny + case_railguard_approval_becomes_deny + case_railguard_outside_fence + case_validate_bash_safe + case_validate_bash_bulk_stage + case_validate_bash_patch_text + case_validate_write_sensitive_patch + case_shellcheck_valid_patch + case_shellcheck_invalid_patch + case_test_runner_patch +) +TOTAL_CASES=${#CASE_NAMES[@]} +RAILGUARD_VERSION=$("$RAILGUARD_BIN" --version 2>/dev/null || printf 'unknown') +printf 'TAP version 13\n' +printf '1..%d\n' "$TOTAL_CASES" +printf '# railguard=%s\n' "$RAILGUARD_BIN" +printf '# version=%s\n' "$RAILGUARD_VERSION" +printf '# workspace=%s\n' "$BENCH_ROOT" + +for (( case_offset = 0; case_offset < TOTAL_CASES; case_offset++ )); do + run_case "${CASE_NAMES[$case_offset]}" "${CASE_FUNCTIONS[$case_offset]}" +done + +printf '# result=%d passed, %d failed\n' "$(( TOTAL_CASES - FAILURES ))" "$FAILURES" +[[ $FAILURES -eq 0 ]] diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dab43bd..c794a58 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,25 @@ Format loosely follows [Keep a Changelog](https://keepachangelog.com). Remaining work lives in [TODO.md](TODO.md); the design rationale for the unified layout is in [UNIFICATION.md](UNIFICATION.md). +## Unreleased — Codex hook parity + +### Added +- `agentic-ai/Claude/benchmark-codex-hooks.sh` provides a deterministic TAP + benchmark for the installed or development Railguard binary plus the shared + Codex hook payloads. Every case runs in disposable state and reports timing. +- `install.sh` now deploys the shared hook scripts to `~/.codex/hooks/` and + idempotently merges their registrations into `~/.codex/hooks.json` without + replacing Railguard or unrelated user hooks. `validate.sh` checks the links, + registrations, and Codex-specific payload behavior. + +### Fixed +- Write, ShellCheck, and test-runner hooks now extract every file from Codex + `apply_patch` payloads while retaining Claude's `file_path` behavior. The Bash + hook ignores patch content only when the tool identity is actually + `apply_patch`, so a patch-like shell command cannot bypass command checks. +- Linux desktop dry-runs no longer execute `pipx ensurepath`; planned pipx + installs are still printed even when pipx is absent. + ## Unreleased — driftcheck as a nudge ### Changed From aebd6d63adc76567076c61c803693af99b00098c Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 31 Aug 2026 12:01:39 -0700 Subject: [PATCH 06/10] fix(agentic-ai): seed ~/.codex/hooks.json on fresh installs Codex only creates hooks.json once the user configures hooks, so on a machine that has never done so, jq aborted install.sh (set -e) before hook registration and the final settings.json redeploy. Seed an empty object first; the existing jq program already handles {} correctly. Found by running the merged install.sh + validate.sh end to end: validate reported all five Codex hook symlinks present but hooks.json missing. --- agentic-ai/Claude/install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agentic-ai/Claude/install.sh b/agentic-ai/Claude/install.sh index be8b8b1..9123fc5 100755 --- a/agentic-ai/Claude/install.sh +++ b/agentic-ai/Claude/install.sh @@ -185,6 +185,9 @@ printf 'Configuring railguard...\n' "$RAILGUARD_BIN" install CODEX_HOOKS="$HOME/.codex/hooks.json" +# Codex only writes hooks.json once the user configures hooks; seed an empty +# object so registration works on a fresh install instead of aborting the script. +[[ -f "$CODEX_HOOKS" ]] || printf '{}\n' > "$CODEX_HOOKS" CODEX_HOOKS_TMP=$(mktemp "$HOME/.codex/hooks.json.tmp.XXXXXX") trap 'rm -f "$CODEX_HOOKS_TMP"' EXIT jq \ From cb56931e1b7005f0fe1e52105ad5282a23431fc3 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 31 Aug 2026 12:05:00 -0700 Subject: [PATCH 07/10] fix(agentic-ai): isolate the Codex benchmark from live Codex config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Railguard's self-integrity check reads $HOME/.codex/hooks.json and denies every call when that file has hooks but no railguard entry — exactly the state install.sh leaves on a machine where railguard skipped Codex registration. The benchmark then failed 2 cases for reasons unrelated to the code under test. Run railguard with HOME pointed at a bench-local home carrying a minimal railguard-registered hooks.json. --- agentic-ai/Claude/benchmark-codex-hooks.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/agentic-ai/Claude/benchmark-codex-hooks.sh b/agentic-ai/Claude/benchmark-codex-hooks.sh index 5e18157..25180d0 100755 --- a/agentic-ai/Claude/benchmark-codex-hooks.sh +++ b/agentic-ai/Claude/benchmark-codex-hooks.sh @@ -40,13 +40,20 @@ BENCH_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/codex-hook-benchmark.XXXXXX") PROJECT_DIR="$BENCH_ROOT/project" OUTSIDE_DIR="$BENCH_ROOT/outside" RAILGUARD_STATE="$BENCH_ROOT/railguard-state" +# Railguard's self-integrity check reads $HOME/.codex/hooks.json and denies +# everything when hooks exist there without a railguard entry. Point HOME at a +# bench-local copy registered the way `railguard install` leaves it, so results +# never depend on the live machine's Codex configuration. +BENCH_HOME="$BENCH_ROOT/home" cleanup() { rm -rf -- "$BENCH_ROOT" } trap cleanup EXIT -mkdir -p "$PROJECT_DIR/.claude" "$PROJECT_DIR/src" "$OUTSIDE_DIR" "$RAILGUARD_STATE" +mkdir -p "$PROJECT_DIR/.claude" "$PROJECT_DIR/src" "$OUTSIDE_DIR" "$RAILGUARD_STATE" "$BENCH_HOME/.codex" +printf '{"hooks":{"PreToolUse":[{"matcher":"","hooks":[{"type":"command","command":"railguard hook --event PreToolUse"}]}]}}\n' \ + > "$BENCH_HOME/.codex/hooks.json" git init --quiet "$PROJECT_DIR" printf 'true\n' > "$PROJECT_DIR/.claude/test-cmd" printf '#!/usr/bin/env bash\nprintf "benchmark\\n"\n' > "$PROJECT_DIR/valid.sh" @@ -100,6 +107,7 @@ capture_railguard() { '{session_id: $session_id, cwd: $cwd, hook_event_name: "PreToolUse", tool_name: $tool_name, tool_input: $tool_input, tool_use_id: "benchmark"}') printf '%s\n' "$input" | env \ + HOME="$BENCH_HOME" \ RAILGUARD_HOME="$RAILGUARD_STATE" \ RAILGUARD_NO_KILL=1 \ "$RAILGUARD_BIN" hook --client codex --event PreToolUse \ From 36cb07c0142a8a45c8a67d09ea85f82a765f51bb Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 31 Aug 2026 12:05:18 -0700 Subject: [PATCH 08/10] docs: record the hooks.json seeding and benchmark isolation fixes --- docs/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c794a58..17de02b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,6 +22,11 @@ in [UNIFICATION.md](UNIFICATION.md). `apply_patch` payloads while retaining Claude's `file_path` behavior. The Bash hook ignores patch content only when the tool identity is actually `apply_patch`, so a patch-like shell command cannot bypass command checks. +- `install.sh` seeds an empty `~/.codex/hooks.json` on machines where Codex has + never written one, instead of aborting before hook registration. +- The Codex benchmark runs Railguard under a bench-local `$HOME`, so results no + longer depend on the live machine's `~/.codex/hooks.json` (Railguard's + self-integrity check denies everything when that file lacks its entry). - Linux desktop dry-runs no longer execute `pipx ensurepath`; planned pipx installs are still printed even when pipx is absent. From 3673b24dc086d479d7d0bc831553c9381726687c Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 31 Aug 2026 13:38:52 -0700 Subject: [PATCH 09/10] fix(agentic-ai): create ~/.claude before copying settings.json On a HOME that has never run Claude Code, cp aborted install.sh at the first deploy step. Found by exercising install.sh in a disposable fake HOME. --- agentic-ai/Claude/install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agentic-ai/Claude/install.sh b/agentic-ai/Claude/install.sh index 9123fc5..e8b90b7 100755 --- a/agentic-ai/Claude/install.sh +++ b/agentic-ai/Claude/install.sh @@ -18,6 +18,8 @@ fi printf 'Installing from: %s\n' "$REPO_DIR" +mkdir -p "$CLAUDE_DIR" + # settings.json is COPIED, not symlinked: Claude Code rewrites its user # settings at runtime (model switches, plugin installs re-serialize the file), # and a symlink funnels that machine state into the repo as permanent dirt. From 7bb48421ed4dfb60e4b12befe757a516c13f372f Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 31 Aug 2026 13:59:37 -0700 Subject: [PATCH 10/10] docs(agentic-ai): document Codex hook trust gating Live end-to-end testing showed registered hooks are silently skipped by codex exec until Codex records per-hook trust in config.toml (granted interactively). Document the caveat and the --dangerously-bypass-hook-trust escape hatch for non-interactive verification of self-authored hooks. --- agentic-ai/Claude/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/agentic-ai/Claude/README.md b/agentic-ai/Claude/README.md index fa0ee7e..72439db 100644 --- a/agentic-ai/Claude/README.md +++ b/agentic-ai/Claude/README.md @@ -195,6 +195,15 @@ RAILGUARD_BIN=/path/to/railguard/target/debug/railguard \ bash agentic-ai/Claude/validate.sh ``` +Registration alone does not make Codex run the hooks: Codex also requires +per-hook trust, which it records in `config.toml` the first time an interactive +session encounters each hook. Until then `codex exec` silently skips them (the +`hooks` feature is on by default in current Codex; only an explicit +`hooks = false` under `[features]` disables the engine outright). For +non-interactive verification, `codex exec --dangerously-bypass-hook-trust` +runs registered hooks without persisted trust — use it only for hooks you +authored. + The commands below remain useful for quick, individual hook probes: ```bash