diff --git a/config/claude/hooks/rtk-rewrite.sh b/config/claude/hooks/rtk-rewrite.sh index 3ec14e0a7..6dadd97ad 100755 --- a/config/claude/hooks/rtk-rewrite.sh +++ b/config/claude/hooks/rtk-rewrite.sh @@ -11,6 +11,11 @@ # 1 No RTK equivalent → pass through unchanged # 2 Deny rule matched → pass through (Claude Code native deny handles it) # 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user +# +# LOCAL DIVERGENCE FROM UPSTREAM (rtk-ai/rtk @ master): this copy also reads +# commands from Copilot's `.toolArgs` (object or string) and emits a top-level +# `modifiedArgs` alongside `hookSpecificOutput`. scripts/sync-rtk-rewrite.sh will +# OVERWRITE these additions on next run — re-apply or upstream after syncing. # --- Audit logging (opt-in via RTK_HOOK_AUDIT=1) --- _rtk_audit_log() { @@ -32,7 +37,14 @@ fi set -euo pipefail INPUT=$(cat) -CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +# Read command from either Claude (`.tool_input.command`) or Copilot +# (`.toolArgs` as object or stringified JSON) format. +CMD=$(echo "$INPUT" | jq -r ' + .tool_input.command + // (.toolArgs | if type == "object" then .command else empty end) + // (.toolArgs | if type == "string" then (fromjson? | if type == "object" then .command else empty end) else empty end) + // empty +') if [ -z "$CMD" ]; then _rtk_audit_log "skip:empty" "-" @@ -79,29 +91,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') +# `// {}` handles pure-Copilot payloads that have no `.tool_input` field. +ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') +# Build Copilot-format `modifiedArgs` when input used `.toolArgs`. Preserves the +# original toolArgs structure (e.g. `timeout`) with `command` replaced. +TOOL_ARGS_KIND=$(echo "$INPUT" | jq -r '.toolArgs | type') +case "$TOOL_ARGS_KIND" in + object) MODIFIED_ARGS=$(echo "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.toolArgs | .command = $cmd') ;; + string) MODIFIED_ARGS=$(echo "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.toolArgs | ((fromjson? | if type == "object" then .command = $cmd else null end) // null)') ;; + *) MODIFIED_ARGS="null" ;; +esac + if [ "$EXIT_CODE" -eq 3 ]; then - # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "updatedInput": $updated - } - }' + DECISION="" else - # Allow: output the rewrite instruction in Claude Code hook format. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "allow", - "permissionDecisionReason": "RTK auto-rewrite", - "updatedInput": $updated - } - }' + DECISION="allow" fi + +jq -n \ + --argjson updated "$UPDATED_INPUT" \ + --argjson modified "$MODIFIED_ARGS" \ + --arg decision "$DECISION" \ + ' + ({ + hookSpecificOutput: ( + {hookEventName: "PreToolUse", updatedInput: $updated} + + (if $decision != "" then + {permissionDecision: $decision, permissionDecisionReason: "RTK auto-rewrite"} + else {} end) + ) + }) + + (if $modified != null then {modifiedArgs: $modified} else {} end) + ' diff --git a/config/codex/hooks/rtk-rewrite.sh b/config/codex/hooks/rtk-rewrite.sh index 3ec14e0a7..6dadd97ad 100644 --- a/config/codex/hooks/rtk-rewrite.sh +++ b/config/codex/hooks/rtk-rewrite.sh @@ -11,6 +11,11 @@ # 1 No RTK equivalent → pass through unchanged # 2 Deny rule matched → pass through (Claude Code native deny handles it) # 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user +# +# LOCAL DIVERGENCE FROM UPSTREAM (rtk-ai/rtk @ master): this copy also reads +# commands from Copilot's `.toolArgs` (object or string) and emits a top-level +# `modifiedArgs` alongside `hookSpecificOutput`. scripts/sync-rtk-rewrite.sh will +# OVERWRITE these additions on next run — re-apply or upstream after syncing. # --- Audit logging (opt-in via RTK_HOOK_AUDIT=1) --- _rtk_audit_log() { @@ -32,7 +37,14 @@ fi set -euo pipefail INPUT=$(cat) -CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +# Read command from either Claude (`.tool_input.command`) or Copilot +# (`.toolArgs` as object or stringified JSON) format. +CMD=$(echo "$INPUT" | jq -r ' + .tool_input.command + // (.toolArgs | if type == "object" then .command else empty end) + // (.toolArgs | if type == "string" then (fromjson? | if type == "object" then .command else empty end) else empty end) + // empty +') if [ -z "$CMD" ]; then _rtk_audit_log "skip:empty" "-" @@ -79,29 +91,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') +# `// {}` handles pure-Copilot payloads that have no `.tool_input` field. +ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') +# Build Copilot-format `modifiedArgs` when input used `.toolArgs`. Preserves the +# original toolArgs structure (e.g. `timeout`) with `command` replaced. +TOOL_ARGS_KIND=$(echo "$INPUT" | jq -r '.toolArgs | type') +case "$TOOL_ARGS_KIND" in + object) MODIFIED_ARGS=$(echo "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.toolArgs | .command = $cmd') ;; + string) MODIFIED_ARGS=$(echo "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.toolArgs | ((fromjson? | if type == "object" then .command = $cmd else null end) // null)') ;; + *) MODIFIED_ARGS="null" ;; +esac + if [ "$EXIT_CODE" -eq 3 ]; then - # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "updatedInput": $updated - } - }' + DECISION="" else - # Allow: output the rewrite instruction in Claude Code hook format. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "allow", - "permissionDecisionReason": "RTK auto-rewrite", - "updatedInput": $updated - } - }' + DECISION="allow" fi + +jq -n \ + --argjson updated "$UPDATED_INPUT" \ + --argjson modified "$MODIFIED_ARGS" \ + --arg decision "$DECISION" \ + ' + ({ + hookSpecificOutput: ( + {hookEventName: "PreToolUse", updatedInput: $updated} + + (if $decision != "" then + {permissionDecision: $decision, permissionDecisionReason: "RTK auto-rewrite"} + else {} end) + ) + }) + + (if $modified != null then {modifiedArgs: $modified} else {} end) + ' diff --git a/config/copilot/hooks/rtk-rewrite.sh b/config/copilot/hooks/rtk-rewrite.sh index 3ec14e0a7..6dadd97ad 100755 --- a/config/copilot/hooks/rtk-rewrite.sh +++ b/config/copilot/hooks/rtk-rewrite.sh @@ -11,6 +11,11 @@ # 1 No RTK equivalent → pass through unchanged # 2 Deny rule matched → pass through (Claude Code native deny handles it) # 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user +# +# LOCAL DIVERGENCE FROM UPSTREAM (rtk-ai/rtk @ master): this copy also reads +# commands from Copilot's `.toolArgs` (object or string) and emits a top-level +# `modifiedArgs` alongside `hookSpecificOutput`. scripts/sync-rtk-rewrite.sh will +# OVERWRITE these additions on next run — re-apply or upstream after syncing. # --- Audit logging (opt-in via RTK_HOOK_AUDIT=1) --- _rtk_audit_log() { @@ -32,7 +37,14 @@ fi set -euo pipefail INPUT=$(cat) -CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +# Read command from either Claude (`.tool_input.command`) or Copilot +# (`.toolArgs` as object or stringified JSON) format. +CMD=$(echo "$INPUT" | jq -r ' + .tool_input.command + // (.toolArgs | if type == "object" then .command else empty end) + // (.toolArgs | if type == "string" then (fromjson? | if type == "object" then .command else empty end) else empty end) + // empty +') if [ -z "$CMD" ]; then _rtk_audit_log "skip:empty" "-" @@ -79,29 +91,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') +# `// {}` handles pure-Copilot payloads that have no `.tool_input` field. +ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') +# Build Copilot-format `modifiedArgs` when input used `.toolArgs`. Preserves the +# original toolArgs structure (e.g. `timeout`) with `command` replaced. +TOOL_ARGS_KIND=$(echo "$INPUT" | jq -r '.toolArgs | type') +case "$TOOL_ARGS_KIND" in + object) MODIFIED_ARGS=$(echo "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.toolArgs | .command = $cmd') ;; + string) MODIFIED_ARGS=$(echo "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.toolArgs | ((fromjson? | if type == "object" then .command = $cmd else null end) // null)') ;; + *) MODIFIED_ARGS="null" ;; +esac + if [ "$EXIT_CODE" -eq 3 ]; then - # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "updatedInput": $updated - } - }' + DECISION="" else - # Allow: output the rewrite instruction in Claude Code hook format. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "allow", - "permissionDecisionReason": "RTK auto-rewrite", - "updatedInput": $updated - } - }' + DECISION="allow" fi + +jq -n \ + --argjson updated "$UPDATED_INPUT" \ + --argjson modified "$MODIFIED_ARGS" \ + --arg decision "$DECISION" \ + ' + ({ + hookSpecificOutput: ( + {hookEventName: "PreToolUse", updatedInput: $updated} + + (if $decision != "" then + {permissionDecision: $decision, permissionDecisionReason: "RTK auto-rewrite"} + else {} end) + ) + }) + + (if $modified != null then {modifiedArgs: $modified} else {} end) + ' diff --git a/scripts/sync-rtk-rewrite.sh b/scripts/sync-rtk-rewrite.sh index a5982fa37..29cfda23a 100755 --- a/scripts/sync-rtk-rewrite.sh +++ b/scripts/sync-rtk-rewrite.sh @@ -1,5 +1,10 @@ #!/usr/bin/env bash -# sync-rtk-rewrite.sh — Sync rtk-rewrite.sh from upstream rtk repo via raw GitHub. +# sync-rtk-rewrite.sh — check upstream rtk-rewrite.sh for drift. +# +# This repo's hook copies have local Copilot-format support (reads .toolArgs, +# emits .modifiedArgs) that upstream rtk-ai/rtk lacks. To protect those local +# additions, this script no longer overwrites the local copies — it just +# fetches upstream and prints a diff so changes can be ported in manually. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -10,6 +15,16 @@ trap 'rm -f "$tmpfile"' EXIT curl -fsSL "$URL" -o "$tmpfile" -cp "$tmpfile" "$ROOT/config/claude/hooks/rtk-rewrite.sh" -cp "$tmpfile" "$ROOT/config/codex/hooks/rtk-rewrite.sh" -cp "$tmpfile" "$ROOT/config/copilot/hooks/rtk-rewrite.sh" +if diff -q "$tmpfile" "$ROOT/config/claude/hooks/rtk-rewrite.sh" >/dev/null; then + echo "No drift from upstream." + exit 0 +fi + +echo "Upstream drift detected (vs config/claude/hooks/rtk-rewrite.sh):" +diff -u "$tmpfile" "$ROOT/config/claude/hooks/rtk-rewrite.sh" || true +echo +echo "Local copies have Copilot-format support not in upstream. Review the" +echo "diff above and port any non-Copilot changes into all three hook files:" +echo " config/claude/hooks/rtk-rewrite.sh" +echo " config/codex/hooks/rtk-rewrite.sh" +echo " config/copilot/hooks/rtk-rewrite.sh" diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 63db6d375..7b62c5404 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -374,6 +374,8 @@ config/codex/hooks/notify.sh config/codex/hooks/pushover.sh config/codex/hooks/rtk-rewrite.sh config/codex/hooks/security.sh +config/copilot/hooks/rtk-rewrite.sh +config/copilot/hooks/security.sh config/shared/hooks/block-gh-settings.sh config/shared/hooks/block-git-push.sh config/cursor/activate.sh diff --git a/spec/sync_rtk_rewrite_spec.sh b/spec/sync_rtk_rewrite_spec.sh index c11379ef0..f4f0819e8 100644 --- a/spec/sync_rtk_rewrite_spec.sh +++ b/spec/sync_rtk_rewrite_spec.sh @@ -17,4 +17,9 @@ 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 + +It 'keeps Claude and Copilot rtk rewrite hooks in sync' +When run cmp -s "$PWD/config/claude/hooks/rtk-rewrite.sh" "$PWD/config/copilot/hooks/rtk-rewrite.sh" +The status should be success +End End diff --git a/treefmt.toml b/treefmt.toml index bc0450815..b5d2de3e8 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -22,6 +22,7 @@ includes = ["*.sh"] excludes = [ "config/claude/hooks/rtk-rewrite.sh", "config/codex/hooks/rtk-rewrite.sh", + "config/copilot/hooks/rtk-rewrite.sh", ] [formatter.lua]