Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/claude/hooks/auto-switch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Changing || echo 0 to || true reduces the robustness of the script. While grep -c typically outputs 0 even when no matches are found (exiting with status 1), using || true means that if grep fails to produce any output (e.g., due to an internal error or being killed), ACCOUNT_COUNT will be assigned an empty string. This will cause a syntax error in the subsequent numeric comparison [ "$ACCOUNT_COUNT" -lt 2 ]. Reverting to || echo 0 ensures a valid numeric fallback.

Suggested change
ACCOUNT_COUNT=$(cswap --list 2>/dev/null | grep -c '^\s*[0-9]' || true)
ACCOUNT_COUNT=$(cswap --list 2>/dev/null | grep -c '^\s*[0-9]' || echo 0)

if [ "$ACCOUNT_COUNT" -lt 2 ]; then
exit 0
fi
Expand Down
44 changes: 39 additions & 5 deletions config/claude/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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" "-"
Expand Down Expand Up @@ -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")')
Comment on lines +90 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two issues in this block:

  1. The ORIGINAL_INPUT extraction (lines 90-98) is missing .command as a fallback source. This causes other top-level fields to be lost when the input is a simple command object (common in Codex), contradicting the goal of preserving original fields.
  2. The IS_COPILOT_INPUT check (line 100) will cause the script to crash if the input is a JSON string (e.g., "ls"), as has() only works on objects/arrays. Since set -e is active (line 32), the script will terminate prematurely.

Adding a type check and including .command in the fallback chain resolves both issues.

Suggested change
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")')
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '
(
.tool_input
// .tool.input
// .toolArgs
// .toolInput
// (if type == "object" and has("command") then . else {} end)
) | 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 'type == "object" and has("toolName") and has("toolArgs")')
References
  1. Scripts should handle failures gracefully to avoid premature termination, especially when set -e is active or when used in critical execution paths.


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" \
Expand Down
9 changes: 9 additions & 0 deletions config/codex/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of alphabetical order — the rest of [features] is sorted, so this should go above tool_call_mcp_elicitation. Same fix needed in config/codex/config.tpl.toml.

unavailable_dummy_tools = true
undo = true
unified_exec = true
Expand Down
9 changes: 9 additions & 0 deletions config/codex/config.tpl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions config/codex/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 39 additions & 5 deletions config/codex/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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" "-"
Expand Down Expand Up @@ -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")')
Comment on lines +90 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two issues in this block:

  1. The ORIGINAL_INPUT extraction (lines 90-98) is missing .command as a fallback source. This causes other top-level fields to be lost when the input is a simple command object (common in Codex), contradicting the goal of preserving original fields.
  2. The IS_COPILOT_INPUT check (line 100) will cause the script to crash if the input is a JSON string (e.g., "ls"), as has() only works on objects/arrays. Since set -e is active (line 32), the script will terminate prematurely.

Adding a type check and including .command in the fallback chain resolves both issues.

Suggested change
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")')
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '
(
.tool_input
// .tool.input
// .toolArgs
// .toolInput
// (if type == "object" and has("command") then . else {} end)
) | 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 'type == "object" and has("toolName") and has("toolArgs")')
References
  1. Scripts should handle failures gracefully to avoid premature termination, especially when set -e is active or when used in critical execution paths.


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" \
Expand Down
23 changes: 17 additions & 6 deletions config/codex/hooks/security.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
#!/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.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unsafe jq field access can abort the security hook when .tool is not an object. Guard the .tool.name lookup so mixed input shapes don't crash the hook.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At config/codex/hooks/security.sh, line 12:

<comment>Unsafe jq field access can abort the security hook when `.tool` is not an object. Guard the `.tool.name` lookup so mixed input shapes don't crash the hook.</comment>

<file context>
@@ -1,18 +1,29 @@
-
-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) ;;
</file context>
Suggested change
tool_name=$(echo "$input" | jq -r '.tool.name // .tool_name // .toolName // empty' 2>/dev/null)
tool_name=$(echo "$input" | jq -r '(.tool | if type == "object" then .name else empty end) // .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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The first command-path in the jq fallback chain is not type-safe; it can fail fast and skip all fallback parsing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At config/codex/hooks/security.sh, line 19:

<comment>The first command-path in the jq fallback chain is not type-safe; it can fail fast and skip all fallback parsing.</comment>

<file context>
@@ -1,18 +1,29 @@
+esac
+
+command=$(echo "$input" | jq -r '
+  .tool.input.command
+  // .tool_input.command
+  // (.toolArgs | if type == "object" then .command else empty end)
</file context>

// .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)
Expand Down
10 changes: 10 additions & 0 deletions config/copilot/activate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Copy managed Copilot config into the mutable runtime location.
# Usage: activate.sh <config_json>
set -euo pipefail

CONFIG_JSON="$1"

mkdir -p ~/.copilot
cp -f "$CONFIG_JSON" ~/.copilot/config.json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wipes Copilot-written state on every activation: config/copilot/default.nix:3 calls out that "Copilot CLI mutates config.json", which is why a Nix symlink isn't used — but cp -f then unconditionally overwrites the runtime file every home-manager switch, blowing away whatever Copilot wrote (auth tokens, banner, model prefs, etc.). The new spec/activate_config_spec.sh "replaces existing config with the managed config" case explicitly tests-and-locks this: a pre-existing "banner": "never" and an extra hook are gone after activation.

If preserving Copilot's mutations matters, merge instead of overwrite, e.g.:

jq -s '.[0] * .[1]' ~/.copilot/config.json "$CONFIG_JSON" > ~/.copilot/config.json.new \
  && mv -f ~/.copilot/config.json.new ~/.copilot/config.json

(or restrict the merge to just hooks and disableAllHooks). If wholesale overwrite is intended, the default.nix comment should reflect that.

chmod 600 ~/.copilot/config.json
34 changes: 34 additions & 0 deletions config/copilot/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing matcher breaks Copilot/Codex parity: this dcg hook (and the block-git-push.sh / block-gh-settings.sh entries below) has no matcher field, so Copilot will run it for every tool call — file reads, MCP, etc. — not just shell. The Codex chain in config/codex/hooks.json:22-65 keeps the entire group under "matcher": "Bash", so on Codex dcg only fires for Bash. Add "matcher": "bash|shell|Bash" here (and to the two block-* entries) to match the sibling rtk/security entries and the Codex chain.

{
  "type": "command",
  "matcher": "bash|shell|Bash",
  "command": "command -v dcg >/dev/null 2>&1 && dcg",
  "timeout": 5
}

"timeout": 5
},
Comment on lines +11 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict dcg to shell tools here too.

This is the only pre-tool hook in this chain without a matcher, so Copilot will run it for every tool call. The stack contract for this PR keeps dcg in the Bash-only path on the Codex side, so this currently breaks the parity goal and can make non-shell tools pay the hook cost or get blocked unexpectedly.

💡 Proposed fix
       {
         "type": "command",
+        "matcher": "bash|shell|Bash",
         "command": "command -v dcg >/dev/null 2>&1 && dcg",
         "timeout": 5
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"type": "command",
"command": "command -v dcg >/dev/null 2>&1 && dcg",
"timeout": 5
},
{
"type": "command",
"matcher": "bash|shell|Bash",
"command": "command -v dcg >/dev/null 2>&1 && dcg",
"timeout": 5
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/copilot/config.json` around lines 11 - 15, The pre-tool hook that runs
"command -v dcg >/dev/null 2>&1 && dcg" is missing a matcher and thus runs for
every tool; restrict it to shell-only tools by adding a matcher entry to that
hook (e.g., add a "matcher" key that allows only shell/bash tools) so the
command-type hook only executes for shell tool invocations and does not run for
non-shell tools.

{
"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",
"timeout": 5
},
{
"type": "command",
"command": "$HOME/dotfiles/config/shared/hooks/block-gh-settings.sh",
"timeout": 5
}
]
}
}
19 changes: 19 additions & 0 deletions config/copilot/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{ lib, pkgs, ... }:
{
# 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}"
'';

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;
};
}
1 change: 1 addition & 0 deletions config/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ in
./ccs
./cliproxyapi
./codex
./copilot
./crush
./cursor
./claude
Expand Down
6 changes: 3 additions & 3 deletions config/shared/hooks/block-gh-settings.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/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

# 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 <destructive-subcommand>
Expand Down
6 changes: 3 additions & 3 deletions config/shared/hooks/block-git-push.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This jq path can throw on valid inputs (e.g. when tool is a string), causing the hook to exit early under set -euo pipefail instead of reading fallback command fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At config/shared/hooks/block-git-push.sh, line 17:

<comment>This jq path can throw on valid inputs (e.g. when `tool` is a string), causing the hook to exit early under `set -euo pipefail` instead of reading fallback command fields.</comment>

<file context>
@@ -13,8 +13,8 @@ ALLOWED_REPOS=(
-# 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
 
</file context>
Suggested change
command=$(echo "$input" | jq -r '.tool.input.command // .tool_input.command // .toolArgs.command // .toolInput.command // .command // empty' 2>/dev/null)
command=$(echo "$input" | jq -r '.tool.input.command? // .tool_input.command? // .toolArgs.command? // .toolInput.command? // .command? // empty' 2>/dev/null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot string toolArgs not blocked: this only reads .toolArgs.command (object form). When Copilot sends toolArgs as a JSON-encoded string — the variant config/codex/hooks/security.sh and config/codex/hooks/rtk-rewrite.sh explicitly handle via fromjson — jq errors with Cannot index string with string "command", the error is swallowed by 2>/dev/null, command is empty, and [[ -z $command ]] && exit 0 lets the push through.

Reproduce:

echo '{"toolName":"shell","toolArgs":"{\"command\":\"git push origin main\"}"}' \
  | jq -r '.tool.input.command // .tool_input.command // .toolArgs.command // .toolInput.command // .command // empty' 2>/dev/null
# -> empty

Replace the extraction with the same union expression used in config/codex/hooks/rtk-rewrite.sh:34-43 (object + string branches). Same fix needed in config/shared/hooks/block-gh-settings.sh:10.

[[ -z $command ]] && exit 0

# Only check git push commands
Expand Down
Loading
Loading