From 0331f1237a1020e98afaebae7e356fcbecf6c97b Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 20:33:39 +0800 Subject: [PATCH 1/7] feat: add Copilot and Codex hook parity - enable new Codex feature flags in managed config - register dcg in Codex and Copilot pre-tool hooks - let shared GitHub blockers parse Copilot hook input Co-authored-by: Codex --- config/claude/hooks/auto-switch.sh | 2 +- config/codex/config.toml | 9 ++++ config/codex/config.tpl.toml | 9 ++++ config/codex/hooks.json | 5 ++ config/copilot/activate.sh | 59 ++++++++++++++++++++++++ config/copilot/config.json | 22 +++++++++ config/copilot/default.nix | 7 +++ config/default.nix | 1 + config/shared/hooks/block-gh-settings.sh | 6 +-- config/shared/hooks/block-git-push.sh | 6 +-- spec/activate_config_spec.sh | 50 ++++++++++++++++++++ spec/block_gh_settings_spec.sh | 11 +++++ spec/block_git_push_spec.sh | 11 +++++ spec/coverage_spec.sh | 1 + 14 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 config/copilot/activate.sh create mode 100644 config/copilot/config.json create mode 100644 config/copilot/default.nix diff --git a/config/claude/hooks/auto-switch.sh b/config/claude/hooks/auto-switch.sh index 1741e529d..5493471cd 100755 --- a/config/claude/hooks/auto-switch.sh +++ b/config/claude/hooks/auto-switch.sh @@ -13,7 +13,7 @@ if ! command -v cswap &>/dev/null; then fi # Require at least 2 managed accounts -ACCOUNT_COUNT=$(cswap --list 2>/dev/null | grep -c '^\s*[0-9]' || echo 0) +ACCOUNT_COUNT=$(cswap --list 2>/dev/null | grep -c '^\s*[0-9]' || true) if [ "$ACCOUNT_COUNT" -lt 2 ]; then exit 0 fi diff --git a/config/codex/config.toml b/config/codex/config.toml index 1e8451d2c..e4ae24323 100644 --- a/config/codex/config.toml +++ b/config/codex/config.toml @@ -8,8 +8,12 @@ oss_provider = "lmstudio" apply_patch_freeform = true apply_patch_streaming_events = true apps = true +apps_mcp_path_override = true artifact = true +auth_elicitation = true browser_use = true +browser_use_external = true +builtin_mcp = true child_agents_md = true chronicle = true code_mode = true @@ -18,6 +22,7 @@ codex_git_commit = true computer_use = true default_mode_request_user_input = true enable_fanout = true +enable_mcp_apps = true enable_request_compression = true exec_permission_approvals = true external_migration = true @@ -34,12 +39,15 @@ memories = true multi_agent = true multi_agent_v2 = true personality = true +plugin_hooks = true plugins = true prevent_idle_sleep = true realtime_conversation = true +remote_compaction_v2 = true remote_control = true remote_plugin = true request_permissions_tool = true +responses_websocket_response_processed = true runtime_metrics = true shell_snapshot = true shell_tool = true @@ -50,6 +58,7 @@ tool_call_mcp_elicitation = true tool_search = true tool_search_always_defer_mcp_tools = true tool_suggest = true +terminal_resize_reflow = true unavailable_dummy_tools = true undo = true unified_exec = true diff --git a/config/codex/config.tpl.toml b/config/codex/config.tpl.toml index 453370dac..f59b6e040 100644 --- a/config/codex/config.tpl.toml +++ b/config/codex/config.tpl.toml @@ -8,8 +8,12 @@ oss_provider = "lmstudio" apply_patch_freeform = true apply_patch_streaming_events = true apps = true +apps_mcp_path_override = true artifact = true +auth_elicitation = true browser_use = true +browser_use_external = true +builtin_mcp = true child_agents_md = true chronicle = true code_mode = true @@ -18,6 +22,7 @@ codex_git_commit = true computer_use = true default_mode_request_user_input = true enable_fanout = true +enable_mcp_apps = true enable_request_compression = true exec_permission_approvals = true external_migration = true @@ -34,12 +39,15 @@ memories = true multi_agent = true multi_agent_v2 = true personality = true +plugin_hooks = true plugins = true prevent_idle_sleep = true realtime_conversation = true +remote_compaction_v2 = true remote_control = true remote_plugin = true request_permissions_tool = true +responses_websocket_response_processed = true runtime_metrics = true shell_snapshot = true shell_tool = true @@ -50,6 +58,7 @@ tool_call_mcp_elicitation = true tool_search = true tool_search_always_defer_mcp_tools = true tool_suggest = true +terminal_resize_reflow = true unavailable_dummy_tools = true undo = true unified_exec = true diff --git a/config/codex/hooks.json b/config/codex/hooks.json index 48e722e49..a08e38bbd 100644 --- a/config/codex/hooks.json +++ b/config/codex/hooks.json @@ -28,6 +28,11 @@ "command": "$HOME/.codex/hooks/rtk-rewrite.sh", "timeout": 5 }, + { + "type": "command", + "command": "command -v dcg >/dev/null 2>&1 && dcg", + "timeout": 5 + }, { "type": "command", "command": "$HOME/.codex/hooks/security.sh", diff --git a/config/copilot/activate.sh b/config/copilot/activate.sh new file mode 100644 index 000000000..d5aa27ea1 --- /dev/null +++ b/config/copilot/activate.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Merge managed Copilot config into the mutable user config. +# Usage: activate.sh [jq_bin] +set -euo pipefail + +MANAGED_CONFIG_JSON="$1" +JQ_BIN="${2:-jq}" +COPILOT_DIR="${HOME}/.copilot" +CONFIG_JSON="${COPILOT_DIR}/config.json" + +mkdir -p "$COPILOT_DIR" + +if [ ! -f "$CONFIG_JSON" ]; then + cp -f "$MANAGED_CONFIG_JSON" "$CONFIG_JSON" + chmod 600 "$CONFIG_JSON" + exit 0 +fi + +tmp=$(mktemp "${CONFIG_JSON}.tmp.XXXXXX") +trap 'rm -f "$tmp"' EXIT + +# shellcheck disable=SC2016 +"$JQ_BIN" -s ' + def hook_key: + [ + (.type // ""), + (.command // ""), + (.bash // ""), + (.powershell // "") + ] | join("\\u0000"); + + def dedupe_hooks: + reduce .[] as $hook ( + { seen: {}, out: [] }; + ($hook | hook_key) as $key | + if .seen[$key] then + . + else + .seen[$key] = true | .out += [$hook] + end + ) | .out; + + .[0] as $current | + .[1] as $managed | + ($current * ($managed | del(.hooks))) as $base | + $base + { + hooks: ( + ($current.hooks // {}) as $existing | + ($managed.hooks // {}) as $new | + reduce (($existing + $new) | keys_unsorted[]) as $event ( + {}; + .[$event] = ((($existing[$event] // []) + ($new[$event] // [])) | dedupe_hooks) + ) + ) + } +' "$CONFIG_JSON" "$MANAGED_CONFIG_JSON" >"$tmp" + +cp -f "$tmp" "$CONFIG_JSON" +chmod 600 "$CONFIG_JSON" diff --git a/config/copilot/config.json b/config/copilot/config.json new file mode 100644 index 000000000..93db269f2 --- /dev/null +++ b/config/copilot/config.json @@ -0,0 +1,22 @@ +{ + "disableAllHooks": false, + "hooks": { + "preToolUse": [ + { + "type": "command", + "command": "command -v dcg >/dev/null 2>&1 && dcg", + "timeout": 5 + }, + { + "type": "command", + "command": "$HOME/dotfiles/config/shared/hooks/block-git-push.sh", + "timeout": 5 + }, + { + "type": "command", + "command": "$HOME/dotfiles/config/shared/hooks/block-gh-settings.sh", + "timeout": 5 + } + ] + } +} diff --git a/config/copilot/default.nix b/config/copilot/default.nix new file mode 100644 index 000000000..db1f4357f --- /dev/null +++ b/config/copilot/default.nix @@ -0,0 +1,7 @@ +{ lib, pkgs, ... }: +{ + # Copilot CLI mutates config.json, so merge managed hooks into the live file. + home.activation.copilotConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${./activate.sh}" "${./config.json}" "${pkgs.jq}/bin/jq" + ''; +} diff --git a/config/default.nix b/config/default.nix index e8cb51277..ad41ffe7c 100644 --- a/config/default.nix +++ b/config/default.nix @@ -9,6 +9,7 @@ in ./ccs ./cliproxyapi ./codex + ./copilot ./crush ./cursor ./claude diff --git a/config/shared/hooks/block-gh-settings.sh b/config/shared/hooks/block-gh-settings.sh index aa30432a7..f1a0db847 100755 --- a/config/shared/hooks/block-gh-settings.sh +++ b/config/shared/hooks/block-gh-settings.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# block-gh-settings.sh — Shared hook for Claude Code + Codex +# block-gh-settings.sh — Shared hook for Claude Code + Codex + Copilot # Blocks gh CLI commands that modify GitHub repository settings. # Exit 2 = block (Codex), JSON decision output (Claude). set -euo pipefail @@ -7,8 +7,8 @@ set -euo pipefail # Read tool input from stdin input=$(cat) -# Extract command -command=$(echo "$input" | jq -r '.tool_input.command // .command // empty' 2>/dev/null) +# Extract command (works for Claude, Codex, and Copilot hook input formats) +command=$(echo "$input" | jq -r '.tool.input.command // .tool_input.command // .toolArgs.command // .toolInput.command // .command // empty' 2>/dev/null) [[ -z $command ]] && exit 0 # Block: gh repo diff --git a/config/shared/hooks/block-git-push.sh b/config/shared/hooks/block-git-push.sh index 0229de510..43c286cad 100755 --- a/config/shared/hooks/block-git-push.sh +++ b/config/shared/hooks/block-git-push.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# block-git-push.sh - Shared hook for Claude Code + Codex +# block-git-push.sh - Shared hook for Claude Code + Codex + Copilot # Blocks git push to main/master unless repo is in the allowlist. # Exit 2 = block (Codex), JSON decision output (Claude). set -euo pipefail @@ -13,8 +13,8 @@ ALLOWED_REPOS=( # Read tool input from stdin input=$(cat) -# Extract command (works for both Claude and Codex input formats) -command=$(echo "$input" | jq -r '.tool_input.command // .command // empty' 2>/dev/null) +# Extract command (works for Claude, Codex, and Copilot hook input formats) +command=$(echo "$input" | jq -r '.tool.input.command // .tool_input.command // .toolArgs.command // .toolInput.command // .command // empty' 2>/dev/null) [[ -z $command ]] && exit 0 # Only check git push commands diff --git a/spec/activate_config_spec.sh b/spec/activate_config_spec.sh index 16efa3491..02cae74bf 100644 --- a/spec/activate_config_spec.sh +++ b/spec/activate_config_spec.sh @@ -34,6 +34,56 @@ It 'registers the shared GitHub settings blocker' When run jq -r '.hooks.PreToolUse[] | select(.matcher == "Bash") | .hooks[].command' "$HOOKS_JSON" The output should include 'config/shared/hooks/block-gh-settings.sh' End + +It 'registers dcg in the Bash pre-tool hook chain' +When run jq -r '.hooks.PreToolUse[] | select(.matcher == "Bash") | .hooks[].command' "$HOOKS_JSON" +The output should include 'command -v dcg >/dev/null 2>&1 && dcg' +End +End + +Describe 'config/copilot/activate.sh' +SCRIPT="$PWD/config/copilot/activate.sh" +CONFIG_JSON="$PWD/config/copilot/config.json" + +It 'uses bash shebang' +When run bash -c "head -1 '$SCRIPT'" +The output should include '#!/usr/bin/env bash' +End + +It 'creates .copilot directory' +When run bash -c "grep 'mkdir -p' '$SCRIPT'" +The output should include 'COPILOT_DIR' +End + +It 'registers dcg in the pre-tool hook chain' +When run jq -r '.hooks.preToolUse[].command' "$CONFIG_JSON" +The output should include 'command -v dcg >/dev/null 2>&1 && dcg' +End + +It 'preserves existing config while merging managed hooks' +TMP_HOME="$(mktemp -d)" +mkdir -p "$TMP_HOME/.copilot" +cat >"$TMP_HOME/.copilot/config.json" <<'JSON' +{ + "banner": "never", + "hooks": { + "preToolUse": [ + { + "type": "command", + "command": "existing-hook", + "timeout": 1 + } + ] + } +} +JSON + +When run bash -c 'HOME="$1" bash "$2" "$3" jq && jq -r ".banner, (.hooks.preToolUse[].command)" "$1/.copilot/config.json"' _ "$TMP_HOME" "$SCRIPT" "$CONFIG_JSON" +The status should be success +The output should include 'never' +The output should include 'existing-hook' +The output should include 'command -v dcg >/dev/null 2>&1 && dcg' +End End Describe 'config/claude/activate.sh' diff --git a/spec/block_gh_settings_spec.sh b/spec/block_gh_settings_spec.sh index 4c107ceaa..b479429a2 100644 --- a/spec/block_gh_settings_spec.sh +++ b/spec/block_gh_settings_spec.sh @@ -113,6 +113,17 @@ End End +Describe 'copilot input format' + +It 'blocks Copilot gh repo edit' +Data '{"toolName": "shell", "toolArgs": {"command": "gh repo edit owner/repo --visibility private"}}' +When run bash "$SCRIPT" +The status should eq 2 +The stderr should include 'BLOCKED' +End + +End + Describe 'edge cases' It 'passes with empty input' diff --git a/spec/block_git_push_spec.sh b/spec/block_git_push_spec.sh index 0000e4edb..06778c591 100644 --- a/spec/block_git_push_spec.sh +++ b/spec/block_git_push_spec.sh @@ -103,6 +103,17 @@ The stderr should include 'BLOCKED' End End +Describe 'copilot input format' +Before 'setup' + +It 'blocks Copilot shell push to main' +Data '{"toolName": "shell", "toolArgs": {"command": "git push origin main"}}' +When run bash -c "cd '$TEMP_REPO' && bash '$SCRIPT'" +The status should eq 2 +The stderr should include 'BLOCKED' +End +End + Describe 'edge cases' Before 'setup' diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 4e125f453..63db6d375 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -367,6 +367,7 @@ config/claude/hooks/security.sh config/claude/hooks/atuin-history.sh config/claude/hooks/statusline.sh config/codex/activate.sh +config/copilot/activate.sh config/codex/hooks/atuin-history.sh config/dcg/activate.sh config/codex/hooks/notify.sh From da2f6d5a5aa72675d66ec65b9123bf3c466ff501 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 20:45:07 +0800 Subject: [PATCH 2/7] fix: clean up Copilot config merge hook Co-authored-by: Codex --- config/copilot/activate.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/copilot/activate.sh b/config/copilot/activate.sh index d5aa27ea1..d3d4cac31 100644 --- a/config/copilot/activate.sh +++ b/config/copilot/activate.sh @@ -19,15 +19,15 @@ fi tmp=$(mktemp "${CONFIG_JSON}.tmp.XXXXXX") trap 'rm -f "$tmp"' EXIT -# shellcheck disable=SC2016 -"$JQ_BIN" -s ' +jq_filter=$( + cat <<'JQ' def hook_key: [ (.type // ""), (.command // ""), (.bash // ""), (.powershell // "") - ] | join("\\u0000"); + ] | join("\u0000"); def dedupe_hooks: reduce .[] as $hook ( @@ -53,7 +53,10 @@ trap 'rm -f "$tmp"' EXIT ) ) } -' "$CONFIG_JSON" "$MANAGED_CONFIG_JSON" >"$tmp" +JQ +) + +"$JQ_BIN" -s "$jq_filter" "$CONFIG_JSON" "$MANAGED_CONFIG_JSON" >"$tmp" cp -f "$tmp" "$CONFIG_JSON" chmod 600 "$CONFIG_JSON" From 8939e4b19ad03f236753db0651d18688a98c19d5 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 20:46:52 +0800 Subject: [PATCH 3/7] fix: avoid temp file in Copilot activation Co-authored-by: Codex --- config/copilot/activate.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/config/copilot/activate.sh b/config/copilot/activate.sh index d3d4cac31..be4fd9bd5 100644 --- a/config/copilot/activate.sh +++ b/config/copilot/activate.sh @@ -16,9 +16,6 @@ if [ ! -f "$CONFIG_JSON" ]; then exit 0 fi -tmp=$(mktemp "${CONFIG_JSON}.tmp.XXXXXX") -trap 'rm -f "$tmp"' EXIT - jq_filter=$( cat <<'JQ' def hook_key: @@ -56,7 +53,6 @@ jq_filter=$( JQ ) -"$JQ_BIN" -s "$jq_filter" "$CONFIG_JSON" "$MANAGED_CONFIG_JSON" >"$tmp" - -cp -f "$tmp" "$CONFIG_JSON" +merged_config=$("$JQ_BIN" -s "$jq_filter" "$CONFIG_JSON" "$MANAGED_CONFIG_JSON") +printf '%s\n' "$merged_config" >"$CONFIG_JSON" chmod 600 "$CONFIG_JSON" From eb1556929ea71218c05c95514a361c1c3da87662 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 20:53:25 +0800 Subject: [PATCH 4/7] fix: simplify Copilot activation config Co-authored-by: Codex --- config/copilot/activate.sh | 60 ++++-------------------------------- config/copilot/default.nix | 4 +-- spec/activate_config_spec.sh | 9 +++--- 3 files changed, 12 insertions(+), 61 deletions(-) diff --git a/config/copilot/activate.sh b/config/copilot/activate.sh index be4fd9bd5..262363e9e 100644 --- a/config/copilot/activate.sh +++ b/config/copilot/activate.sh @@ -1,58 +1,10 @@ #!/usr/bin/env bash -# Merge managed Copilot config into the mutable user config. -# Usage: activate.sh [jq_bin] +# Copy managed Copilot config into the mutable runtime location. +# Usage: activate.sh set -euo pipefail -MANAGED_CONFIG_JSON="$1" -JQ_BIN="${2:-jq}" -COPILOT_DIR="${HOME}/.copilot" -CONFIG_JSON="${COPILOT_DIR}/config.json" +CONFIG_JSON="$1" -mkdir -p "$COPILOT_DIR" - -if [ ! -f "$CONFIG_JSON" ]; then - cp -f "$MANAGED_CONFIG_JSON" "$CONFIG_JSON" - chmod 600 "$CONFIG_JSON" - exit 0 -fi - -jq_filter=$( - cat <<'JQ' - def hook_key: - [ - (.type // ""), - (.command // ""), - (.bash // ""), - (.powershell // "") - ] | join("\u0000"); - - def dedupe_hooks: - reduce .[] as $hook ( - { seen: {}, out: [] }; - ($hook | hook_key) as $key | - if .seen[$key] then - . - else - .seen[$key] = true | .out += [$hook] - end - ) | .out; - - .[0] as $current | - .[1] as $managed | - ($current * ($managed | del(.hooks))) as $base | - $base + { - hooks: ( - ($current.hooks // {}) as $existing | - ($managed.hooks // {}) as $new | - reduce (($existing + $new) | keys_unsorted[]) as $event ( - {}; - .[$event] = ((($existing[$event] // []) + ($new[$event] // [])) | dedupe_hooks) - ) - ) - } -JQ -) - -merged_config=$("$JQ_BIN" -s "$jq_filter" "$CONFIG_JSON" "$MANAGED_CONFIG_JSON") -printf '%s\n' "$merged_config" >"$CONFIG_JSON" -chmod 600 "$CONFIG_JSON" +mkdir -p ~/.copilot +cp -f "$CONFIG_JSON" ~/.copilot/config.json +chmod 600 ~/.copilot/config.json diff --git a/config/copilot/default.nix b/config/copilot/default.nix index db1f4357f..3741d4ddd 100644 --- a/config/copilot/default.nix +++ b/config/copilot/default.nix @@ -1,7 +1,7 @@ { lib, pkgs, ... }: { - # Copilot CLI mutates config.json, so merge managed hooks into the live file. + # Copilot CLI mutates config.json, so copy the managed file into place. home.activation.copilotConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${./activate.sh}" "${./config.json}" "${pkgs.jq}/bin/jq" + $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${./activate.sh}" "${./config.json}" ''; } diff --git a/spec/activate_config_spec.sh b/spec/activate_config_spec.sh index 02cae74bf..6bb6f7fcc 100644 --- a/spec/activate_config_spec.sh +++ b/spec/activate_config_spec.sh @@ -52,7 +52,7 @@ End It 'creates .copilot directory' When run bash -c "grep 'mkdir -p' '$SCRIPT'" -The output should include 'COPILOT_DIR' +The output should include '.copilot' End It 'registers dcg in the pre-tool hook chain' @@ -60,7 +60,7 @@ When run jq -r '.hooks.preToolUse[].command' "$CONFIG_JSON" The output should include 'command -v dcg >/dev/null 2>&1 && dcg' End -It 'preserves existing config while merging managed hooks' +It 'replaces existing config with the managed config' TMP_HOME="$(mktemp -d)" mkdir -p "$TMP_HOME/.copilot" cat >"$TMP_HOME/.copilot/config.json" <<'JSON' @@ -78,10 +78,9 @@ cat >"$TMP_HOME/.copilot/config.json" <<'JSON' } JSON -When run bash -c 'HOME="$1" bash "$2" "$3" jq && jq -r ".banner, (.hooks.preToolUse[].command)" "$1/.copilot/config.json"' _ "$TMP_HOME" "$SCRIPT" "$CONFIG_JSON" +When run bash -c 'HOME="$1" bash "$2" "$3" && jq -r ".disableAllHooks, (.hooks.preToolUse[].command)" "$1/.copilot/config.json"' _ "$TMP_HOME" "$SCRIPT" "$CONFIG_JSON" The status should be success -The output should include 'never' -The output should include 'existing-hook' +The output should include 'false' The output should include 'command -v dcg >/dev/null 2>&1 && dcg' End End From 8c35dbd63e7976d8e6b2f82e37b7f2c4cd3503b7 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 20:58:08 +0800 Subject: [PATCH 5/7] feat: add Codex remote-control fish wrapper Co-authored-by: Codex --- home-manager/programs/fish/default.nix | 2 ++ .../fish/functions/_corc_function.fish | 9 ++++++ spec/fish/_corc_function_test.fish | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 home-manager/programs/fish/functions/_corc_function.fish create mode 100644 spec/fish/_corc_function_test.fish diff --git a/home-manager/programs/fish/default.nix b/home-manager/programs/fish/default.nix index 59284e4cc..e1d6945d3 100644 --- a/home-manager/programs/fish/default.nix +++ b/home-manager/programs/fish/default.nix @@ -112,6 +112,7 @@ clwxeh = "_clwxeh_function"; clxe = "_clxe_function"; clxeh = "_clxeh_function"; + corc = "_corc_function"; coxe = "_coxe_function"; coxeh = "_coxeh_function"; coxel = "_coxel_function"; @@ -224,6 +225,7 @@ "_clwxeh_function" "_clxe_function" "_clxeh_function" + "_corc_function" "_coxe_function" "_coxeh_function" "_coxel_function" diff --git a/home-manager/programs/fish/functions/_corc_function.fish b/home-manager/programs/fish/functions/_corc_function.fish new file mode 100644 index 000000000..6264df12d --- /dev/null +++ b/home-manager/programs/fish/functions/_corc_function.fish @@ -0,0 +1,9 @@ +function _corc_function --description "Run Codex remote-control with a stable binary" + # Resolve the symlink to the real inode before starting. + # When bun replaces the file (creates a new inode), the running node process + # keeps its reference to the old inode and is unaffected. + # Usage: corc [] + + set -l codex_real (realpath (which codex)) + $codex_real remote-control $argv +end diff --git a/spec/fish/_corc_function_test.fish b/spec/fish/_corc_function_test.fish new file mode 100644 index 000000000..c806f51f7 --- /dev/null +++ b/spec/fish/_corc_function_test.fish @@ -0,0 +1,32 @@ +set fn (status dirname)/../../home-manager/programs/fish/functions +source $fn/_corc_function.fish + +# -- basic: resolves symlink and runs codex remote-control --------------------- +set log1 (mktemp) +set fake_cli (mktemp) +chmod +x $fake_cli +echo '#!/bin/sh +echo "$0" "$@" >> '$log1 >$fake_cli + +function which; echo $fake_cli; end +function realpath; echo $argv[1]; end + +_corc_function + +@test "runs resolved binary directly" (grep -c $fake_cli $log1) -ge 1 +@test "passes remote-control subcommand" (grep -c "remote-control" $log1) -ge 1 +@test "skips Claude-only worktree spawn flag" (grep -c -- "--spawn" $log1) -eq 0 +@test "skips Claude-only permission mode flag" (grep -c -- "--permission-mode" $log1) -eq 0 + +# -- with args: passes through Codex remote-control args ----------------------- +set log2 (mktemp) +echo '#!/bin/sh +echo "$0" "$@" >> '$log2 >$fake_cli + +_corc_function --enable remote_control + +@test "passes extra args through" (grep -c -- "--enable" $log2) -ge 1 +@test "still includes remote-control with args" (grep -c "remote-control" $log2) -ge 1 +@test "still skips Claude-only worktree spawn flag" (grep -c -- "--spawn" $log2) -eq 0 + +rm -f $log1 $log2 $fake_cli From af749ae0489caa2ea21fbd9ccbc073745d9aa674 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 21:08:33 +0800 Subject: [PATCH 6/7] feat: sync rtk and security hooks to Copilot Co-authored-by: Codex --- config/codex/hooks/rtk-rewrite.sh | 44 +++++++++++++++++++++++++++---- config/codex/hooks/security.sh | 23 +++++++++++----- config/copilot/config.json | 12 +++++++++ config/copilot/default.nix | 12 +++++++++ spec/activate_config_spec.sh | 24 +++++++++++++++++ spec/codex_rtk_rewrite_spec.sh | 22 ++++++++++++++++ spec/codex_security_spec.sh | 20 ++++++++++++++ 7 files changed, 146 insertions(+), 11 deletions(-) diff --git a/config/codex/hooks/rtk-rewrite.sh b/config/codex/hooks/rtk-rewrite.sh index 3ec14e0a7..4a4566931 100644 --- a/config/codex/hooks/rtk-rewrite.sh +++ b/config/codex/hooks/rtk-rewrite.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # rtk-hook-version: 3 -# RTK auto-rewrite hook for Claude Code PreToolUse:Bash +# RTK auto-rewrite hook for Claude/Codex/Copilot PreToolUse shell commands. # Transparently rewrites raw commands to their RTK equivalents. # Uses `rtk rewrite` as single source of truth — no duplicate mapping logic here. # @@ -32,7 +32,15 @@ fi set -euo pipefail INPUT=$(cat) -CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +CMD=$(echo "$INPUT" | jq -r ' + .tool.input.command + // .tool_input.command + // (.toolArgs | if type == "object" then .command else empty end) + // (.toolArgs | if type == "string" then (fromjson? | .command) else empty end) + // .toolInput.command + // .command + // empty +') if [ -z "$CMD" ]; then _rtk_audit_log "skip:empty" "-" @@ -78,11 +86,37 @@ esac _rtk_audit_log "rewrite" "$CMD" "$REWRITTEN" -# Build the updated tool_input with all original fields preserved, only command changed. -ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') +# Build the updated tool input with all original fields preserved, only command changed. +ORIGINAL_INPUT=$(echo "$INPUT" | jq -c ' + ( + .tool_input + // .tool.input + // .toolArgs + // .toolInput + // {} + ) | if type == "string" then (fromjson? // {}) else . end +') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') +IS_COPILOT_INPUT=$(echo "$INPUT" | jq -r 'has("toolName") and has("toolArgs")') -if [ "$EXIT_CODE" -eq 3 ]; then +if [ "$IS_COPILOT_INPUT" = "true" ]; then + if [ "$EXIT_CODE" -eq 3 ]; then + jq -n \ + --argjson modified "$UPDATED_INPUT" \ + '{ + "permissionDecision": "ask", + "modifiedArgs": $modified + }' + else + jq -n \ + --argjson modified "$UPDATED_INPUT" \ + '{ + "permissionDecision": "allow", + "permissionDecisionReason": "RTK auto-rewrite", + "modifiedArgs": $modified + }' + fi +elif [ "$EXIT_CODE" -eq 3 ]; then # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. jq -n \ --argjson updated "$UPDATED_INPUT" \ diff --git a/config/codex/hooks/security.sh b/config/codex/hooks/security.sh index 8391bafd9..ce57d9f96 100755 --- a/config/codex/hooks/security.sh +++ b/config/codex/hooks/security.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Codex Security Hook +# Codex/Copilot Security Hook # Blocks dangerous Bash commands by checking against deny patterns. # Returns exit code 2 to block, exit code 0 to allow. @@ -8,11 +8,22 @@ set -euo pipefail input=$(cat) -# Only process Bash commands -tool_name=$(echo "$input" | jq -r '.tool_name // empty' 2>/dev/null) -[[ $tool_name != "Bash" ]] && exit 0 - -command=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null) +# Only process shell commands when the hook input includes a tool name. +tool_name=$(echo "$input" | jq -r '.tool.name // .tool_name // .toolName // empty' 2>/dev/null) +case "$tool_name" in +"" | Bash | bash | shell) ;; +*) exit 0 ;; +esac + +command=$(echo "$input" | jq -r ' + .tool.input.command + // .tool_input.command + // (.toolArgs | if type == "object" then .command else empty end) + // (.toolArgs | if type == "string" then (fromjson? | .command) else empty end) + // .toolInput.command + // .command + // empty +' 2>/dev/null) [[ -z $command ]] && exit 0 # Hardcoded deny patterns (mirrors claude settings.json deny list) diff --git a/config/copilot/config.json b/config/copilot/config.json index 93db269f2..ac1bb0a01 100644 --- a/config/copilot/config.json +++ b/config/copilot/config.json @@ -2,11 +2,23 @@ "disableAllHooks": false, "hooks": { "preToolUse": [ + { + "type": "command", + "matcher": "bash|shell|Bash", + "command": "$HOME/.copilot/hooks/rtk-rewrite.sh", + "timeout": 5 + }, { "type": "command", "command": "command -v dcg >/dev/null 2>&1 && dcg", "timeout": 5 }, + { + "type": "command", + "matcher": "bash|shell|Bash", + "command": "$HOME/.copilot/hooks/security.sh", + "timeout": 5 + }, { "type": "command", "command": "$HOME/dotfiles/config/shared/hooks/block-git-push.sh", diff --git a/config/copilot/default.nix b/config/copilot/default.nix index 3741d4ddd..aabd7323f 100644 --- a/config/copilot/default.nix +++ b/config/copilot/default.nix @@ -4,4 +4,16 @@ home.activation.copilotConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${./activate.sh}" "${./config.json}" ''; + + home.file.".copilot/hooks/rtk-rewrite.sh" = { + source = ../codex/hooks/rtk-rewrite.sh; + executable = true; + force = true; + }; + + home.file.".copilot/hooks/security.sh" = { + source = ../codex/hooks/security.sh; + executable = true; + force = true; + }; } diff --git a/spec/activate_config_spec.sh b/spec/activate_config_spec.sh index 6bb6f7fcc..9a508b074 100644 --- a/spec/activate_config_spec.sh +++ b/spec/activate_config_spec.sh @@ -60,6 +60,16 @@ When run jq -r '.hooks.preToolUse[].command' "$CONFIG_JSON" The output should include 'command -v dcg >/dev/null 2>&1 && dcg' End +It 'registers rtk rewrite in the pre-tool hook chain' +When run jq -r '.hooks.preToolUse[].command' "$CONFIG_JSON" +The output should include '$HOME/.copilot/hooks/rtk-rewrite.sh' +End + +It 'registers security in the pre-tool hook chain' +When run jq -r '.hooks.preToolUse[].command' "$CONFIG_JSON" +The output should include '$HOME/.copilot/hooks/security.sh' +End + It 'replaces existing config with the managed config' TMP_HOME="$(mktemp -d)" mkdir -p "$TMP_HOME/.copilot" @@ -85,6 +95,20 @@ The output should include 'command -v dcg >/dev/null 2>&1 && dcg' End End +Describe 'config/copilot/default.nix' +DEFAULT_NIX="$PWD/config/copilot/default.nix" + +It 'installs Copilot rtk rewrite hook' +When run cat "$DEFAULT_NIX" +The output should include '.copilot/hooks/rtk-rewrite.sh' +End + +It 'installs Copilot security hook' +When run cat "$DEFAULT_NIX" +The output should include '.copilot/hooks/security.sh' +End +End + Describe 'config/claude/activate.sh' SCRIPT="$PWD/config/claude/activate.sh" diff --git a/spec/codex_rtk_rewrite_spec.sh b/spec/codex_rtk_rewrite_spec.sh index c5c885b89..baa29696f 100644 --- a/spec/codex_rtk_rewrite_spec.sh +++ b/spec/codex_rtk_rewrite_spec.sh @@ -33,6 +33,12 @@ run_hook() { else PATH="$MOCK_BIN:$PATH" bash "$SCRIPT"; fi } +run_hook_modified_args() { + if [ "$HAS_RTK_REWRITE" = true ]; then + bash "$SCRIPT" | jq -r '.modifiedArgs.command' + else PATH="$MOCK_BIN:$PATH" bash "$SCRIPT" | jq -r '.modifiedArgs.command'; fi +} + Describe 'guards' It 'exits silently with empty command' Data '{"tool_input": {}}' @@ -57,6 +63,22 @@ When run run_hook The status should be success The output should eq '' End + +It 'rewrites Copilot object toolArgs with modifiedArgs output' +Data '{"toolName": "shell", "toolArgs": {"command": "git status", "timeout": 5000}}' +When run run_hook_modified_args +The status should be success +The output should include 'rtk' +The output should include 'git status' +End + +It 'rewrites Copilot string toolArgs with modifiedArgs output' +Data '{"toolName": "bash", "toolArgs": "{\"command\":\"git status\",\"timeout\":5000}"}' +When run run_hook_modified_args +The status should be success +The output should include 'rtk' +The output should include 'git status' +End End End diff --git a/spec/codex_security_spec.sh b/spec/codex_security_spec.sh index 7e125dac2..a73aad77e 100644 --- a/spec/codex_security_spec.sh +++ b/spec/codex_security_spec.sh @@ -11,6 +11,12 @@ When run bash "$SCRIPT" The status should be success End +It 'passes non-shell Copilot tools through' +Data '{"toolName": "read", "toolArgs": {"command": "sudo rm -rf /"}}' +When run bash "$SCRIPT" +The status should be success +End + It 'passes when no tool specified' Data '{}' When run bash "$SCRIPT" @@ -60,6 +66,20 @@ When run bash "$SCRIPT" The status should eq 2 The stderr should include 'BLOCKED' End + +It 'blocks Copilot shell input with object toolArgs' +Data '{"toolName": "shell", "toolArgs": {"command": "sudo rm -rf /"}}' +When run bash "$SCRIPT" +The status should eq 2 +The stderr should include 'BLOCKED' +End + +It 'blocks Copilot bash input with string toolArgs' +Data '{"toolName": "bash", "toolArgs": "{\"command\":\"rm -rf /*\"}"}' +When run bash "$SCRIPT" +The status should eq 2 +The stderr should include 'BLOCKED' +End End Describe 'edge cases' From 7e1604c40232d8e837ec6a8967882a5acfbaed13 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 15 May 2026 21:13:37 +0800 Subject: [PATCH 7/7] fix: keep rtk rewrite hooks synced Co-authored-by: Codex --- config/claude/hooks/rtk-rewrite.sh | 44 ++++++++++++++++++++++++++---- spec/rtk_rewrite_spec.sh | 24 ++++++++++++++++ spec/sync_rtk_rewrite_spec.sh | 5 ++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/config/claude/hooks/rtk-rewrite.sh b/config/claude/hooks/rtk-rewrite.sh index 3ec14e0a7..4a4566931 100755 --- a/config/claude/hooks/rtk-rewrite.sh +++ b/config/claude/hooks/rtk-rewrite.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # rtk-hook-version: 3 -# RTK auto-rewrite hook for Claude Code PreToolUse:Bash +# RTK auto-rewrite hook for Claude/Codex/Copilot PreToolUse shell commands. # Transparently rewrites raw commands to their RTK equivalents. # Uses `rtk rewrite` as single source of truth — no duplicate mapping logic here. # @@ -32,7 +32,15 @@ fi set -euo pipefail INPUT=$(cat) -CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +CMD=$(echo "$INPUT" | jq -r ' + .tool.input.command + // .tool_input.command + // (.toolArgs | if type == "object" then .command else empty end) + // (.toolArgs | if type == "string" then (fromjson? | .command) else empty end) + // .toolInput.command + // .command + // empty +') if [ -z "$CMD" ]; then _rtk_audit_log "skip:empty" "-" @@ -78,11 +86,37 @@ esac _rtk_audit_log "rewrite" "$CMD" "$REWRITTEN" -# Build the updated tool_input with all original fields preserved, only command changed. -ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') +# Build the updated tool input with all original fields preserved, only command changed. +ORIGINAL_INPUT=$(echo "$INPUT" | jq -c ' + ( + .tool_input + // .tool.input + // .toolArgs + // .toolInput + // {} + ) | if type == "string" then (fromjson? // {}) else . end +') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') +IS_COPILOT_INPUT=$(echo "$INPUT" | jq -r 'has("toolName") and has("toolArgs")') -if [ "$EXIT_CODE" -eq 3 ]; then +if [ "$IS_COPILOT_INPUT" = "true" ]; then + if [ "$EXIT_CODE" -eq 3 ]; then + jq -n \ + --argjson modified "$UPDATED_INPUT" \ + '{ + "permissionDecision": "ask", + "modifiedArgs": $modified + }' + else + jq -n \ + --argjson modified "$UPDATED_INPUT" \ + '{ + "permissionDecision": "allow", + "permissionDecisionReason": "RTK auto-rewrite", + "modifiedArgs": $modified + }' + fi +elif [ "$EXIT_CODE" -eq 3 ]; then # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. jq -n \ --argjson updated "$UPDATED_INPUT" \ diff --git a/spec/rtk_rewrite_spec.sh b/spec/rtk_rewrite_spec.sh index d91fd3784..306e42a86 100644 --- a/spec/rtk_rewrite_spec.sh +++ b/spec/rtk_rewrite_spec.sh @@ -54,6 +54,14 @@ run_hook_jq() { fi } +run_hook_modified_args() { + if [ "$HAS_RTK_REWRITE" = true ]; then + bash "$SCRIPT" | jq -r '.modifiedArgs.command' + else + PATH="$MOCK_BIN:$PATH" bash "$SCRIPT" | jq -r '.modifiedArgs.command' + fi +} + Describe 'guards' It 'exits silently when no command in input' Data '{"tool_input": {}}' @@ -101,6 +109,22 @@ When run run_hook The status should be success The output should eq '' End + +It 'rewrites Copilot object toolArgs with modifiedArgs output' +Data '{"toolName": "shell", "toolArgs": {"command": "git status", "timeout": 5000}}' +When run run_hook_modified_args +The status should be success +The output should include 'rtk' +The output should include 'git status' +End + +It 'rewrites Copilot string toolArgs with modifiedArgs output' +Data '{"toolName": "bash", "toolArgs": "{\"command\":\"git status\",\"timeout\":5000}"}' +When run run_hook_modified_args +The status should be success +The output should include 'rtk' +The output should include 'git status' +End End Describe 'output format' diff --git a/spec/sync_rtk_rewrite_spec.sh b/spec/sync_rtk_rewrite_spec.sh index 2b97cb203..c11379ef0 100644 --- a/spec/sync_rtk_rewrite_spec.sh +++ b/spec/sync_rtk_rewrite_spec.sh @@ -12,4 +12,9 @@ It 'contains curl to raw.githubusercontent.com' When run bash -c "grep -q 'raw.githubusercontent.com/rtk-ai/rtk' '$SCRIPT' && echo found" The output should eq 'found' End + +It 'keeps Claude and Codex rtk rewrite hooks in sync' +When run cmp -s "$PWD/config/claude/hooks/rtk-rewrite.sh" "$PWD/config/codex/hooks/rtk-rewrite.sh" +The status should be success +End End