Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
64 changes: 42 additions & 22 deletions config/claude/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.

Stale warning after the sync refactor: this banner still says scripts/sync-rtk-rewrite.sh will OVERWRITE these additions on next run, but after 5e79309 the sync script no longer overwrites — it only curls upstream and prints a diff. Worth updating the wording (e.g. “Run scripts/sync-rtk-rewrite.sh to diff against upstream and port any non-Copilot changes in manually”) so future readers don't think their local edits are fragile. Remember to mirror the edit byte-for-byte into the codex and copilot copies so spec/sync_rtk_rewrite_spec.sh stays green.


# --- Audit logging (opt-in via RTK_HOOK_AUDIT=1) ---
_rtk_audit_log() {
Expand All @@ -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? | .command) else empty end)
// empty
')
Comment thread
shunkakinoki marked this conversation as resolved.

if [ -z "$CMD" ]; then
_rtk_audit_log "skip:empty" "-"
Expand Down Expand Up @@ -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 | .command = $cmd') ;;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
shunkakinoki marked this conversation as resolved.
Outdated
*) 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)
'
Comment on lines 93 to +127
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

The current implementation uses multiple jq calls to construct the output JSON, which is inefficient in a shell script due to process forking. Additionally, the fromjson call on line 103 is potentially unsafe if .toolArgs is a string but not valid JSON.

Consolidating these into a single jq call improves performance and simplifies the logic while adding robustness to the JSON parsing.

Suggested change
# 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 | .command = $cmd') ;;
*) 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)
'
# Build the final output JSON, consolidating Claude and Copilot formats.
jq -n \
--argjson input "$INPUT" \
--arg rewritten "$REWRITTEN" \
--argjson is_ask "$([ "$EXIT_CODE" -eq 3 ] && echo true || echo false)" \
'
($input.tool_input // {} | .command = $rewritten) as $updated |
($input.toolArgs | if type == "object" then .command = $rewritten elif type == "string" then (fromjson? | if type == "object" then .command = $rewritten else null end) else null end) as $modified |
{
hookSpecificOutput: (
{hookEventName: "PreToolUse", updatedInput: $updated}
+ (if $is_ask then {} else {permissionDecision: "allow", permissionDecisionReason: "RTK auto-rewrite"} end)
)
}
+ (if $modified != null then {modifiedArgs: $modified} else {} end)
'

64 changes: 42 additions & 22 deletions config/codex/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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? | .command) else empty end)
// empty
')
Comment thread
shunkakinoki marked this conversation as resolved.

if [ -z "$CMD" ]; then
_rtk_audit_log "skip:empty" "-"
Expand Down Expand Up @@ -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 | .command = $cmd') ;;
*) 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)
'
Comment on lines 93 to +127
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

The current implementation uses multiple jq calls to construct the output JSON, which is inefficient in a shell script due to process forking. Additionally, the fromjson call on line 103 is potentially unsafe if .toolArgs is a string but not valid JSON.

Consolidating these into a single jq call improves performance and simplifies the logic while adding robustness to the JSON parsing.

Suggested change
# 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 | .command = $cmd') ;;
*) 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)
'
# Build the final output JSON, consolidating Claude and Copilot formats.
jq -n \
--argjson input "$INPUT" \
--arg rewritten "$REWRITTEN" \
--argjson is_ask "$([ "$EXIT_CODE" -eq 3 ] && echo true || echo false)" \
'
($input.tool_input // {} | .command = $rewritten) as $updated |
($input.toolArgs | if type == "object" then .command = $rewritten elif type == "string" then (fromjson? | if type == "object" then .command = $rewritten else null end) else null end) as $modified |
{
hookSpecificOutput: (
{hookEventName: "PreToolUse", updatedInput: $updated}
+ (if $is_ask then {} else {permissionDecision: "allow", permissionDecisionReason: "RTK auto-rewrite"} end)
)
}
+ (if $modified != null then {modifiedArgs: $modified} else {} end)
'

64 changes: 42 additions & 22 deletions config/copilot/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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? | .command) else empty end)
// empty
')
Comment thread
shunkakinoki marked this conversation as resolved.

if [ -z "$CMD" ]; then
_rtk_audit_log "skip:empty" "-"
Expand Down Expand Up @@ -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 | .command = $cmd') ;;
*) 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)
'
Comment on lines 93 to +127
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

The current implementation uses multiple jq calls to construct the output JSON, which is inefficient in a shell script due to process forking. Additionally, the fromjson call on line 103 is potentially unsafe if .toolArgs is a string but not valid JSON.

Consolidating these into a single jq call improves performance and simplifies the logic while adding robustness to the JSON parsing.

Suggested change
# 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 | .command = $cmd') ;;
*) 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)
'
# Build the final output JSON, consolidating Claude and Copilot formats.
jq -n \
--argjson input "$INPUT" \
--arg rewritten "$REWRITTEN" \
--argjson is_ask "$([ "$EXIT_CODE" -eq 3 ] && echo true || echo false)" \
'
($input.tool_input // {} | .command = $rewritten) as $updated |
($input.toolArgs | if type == "object" then .command = $rewritten elif type == "string" then (fromjson? | if type == "object" then .command = $rewritten else null end) else null end) as $modified |
{
hookSpecificOutput: (
{hookEventName: "PreToolUse", updatedInput: $updated}
+ (if $is_ask then {} else {permissionDecision: "allow", permissionDecisionReason: "RTK auto-rewrite"} end)
)
}
+ (if $modified != null then {modifiedArgs: $modified} else {} end)
'

2 changes: 2 additions & 0 deletions spec/coverage_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading