Skip to content

Commit f2b7249

Browse files
committed
fix: harden adapter and trust handling
1 parent 8bbf922 commit f2b7249

File tree

8 files changed

+220
-22
lines changed

8 files changed

+220
-22
lines changed

lib/adapters.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ _load_adapter() {
341341
# Extract first word (command name) from potentially multi-word string
342342
local cmd_name="${name%% *}"
343343

344+
case "$cmd_name" in
345+
*/* | *\\*)
346+
log_error "$label '$name' must use a PATH command name, not a filesystem path"
347+
log_info "Use a simple command name, optionally with flags (e.g., 'code --wait')"
348+
return 1
349+
;;
350+
esac
351+
344352
if ! command -v "$cmd_name" >/dev/null 2>&1; then
345353
log_error "$label '$name' not found"
346354
log_info "Built-in adapters: $builtin_list"

lib/commands/init.sh

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ cmd_init() {
6868
# Generate output (cached to ~/.cache/gtr/, auto-invalidates on shell integration changes)
6969
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/gtr"
7070
local cache_file="$cache_dir/init-${func_name}.${shell}"
71-
local cache_schema="${GTR_INIT_CACHE_VERSION:-2}"
71+
local cache_schema="${GTR_INIT_CACHE_VERSION:-3}"
7272
local cache_stamp="# gtr-cache: version=${GTR_VERSION:-unknown} init=${cache_schema} func=$func_name shell=$shell"
7373

7474
# Return cached output if version matches
@@ -115,6 +115,14 @@ __FUNC___hooks_hash() {
115115
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
116116
}
117117
118+
__FUNC___hooks_trust_key() {
119+
local _gtr_config_file="$1"
120+
local _gtr_hook_hash _gtr_repo_root
121+
_gtr_hook_hash="$(__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)" || return 1
122+
_gtr_repo_root="$(cd "$(dirname "$_gtr_config_file")" 2>/dev/null && pwd -P)" || return 1
123+
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1
124+
}
125+
118126
__FUNC___run_post_cd_hooks() {
119127
local dir="$1"
120128
local _gtr_trust_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtr/trusted"
@@ -132,9 +140,9 @@ __FUNC___run_post_cd_hooks() {
132140
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
133141
if [ -n "$_gtr_file_hooks" ]; then
134142
# Verify trust before including .gtrconfig hooks
135-
local _gtr_hook_hash
136-
_gtr_hook_hash="$(__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)" || true
137-
if [ -n "$_gtr_hook_hash" ] && [ -f "$_gtr_trust_dir/$_gtr_hook_hash" ]; then
143+
local _gtr_trust_key
144+
_gtr_trust_key="$(__FUNC___hooks_trust_key "$_gtr_config_file" 2>/dev/null)" || true
145+
if [ -n "$_gtr_trust_key" ] && [ -f "$_gtr_trust_dir/$_gtr_trust_key" ]; then
138146
if [ -n "$_gtr_hooks" ]; then
139147
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
140148
else
@@ -321,6 +329,15 @@ __FUNC___hooks_hash() {
321329
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
322330
}
323331
332+
__FUNC___hooks_trust_key() {
333+
emulate -L zsh
334+
local _gtr_config_file="$1"
335+
local _gtr_hook_hash _gtr_repo_root
336+
_gtr_hook_hash="$(__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)" || return 1
337+
_gtr_repo_root="$(cd "$(dirname "$_gtr_config_file")" 2>/dev/null && pwd -P)" || return 1
338+
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1
339+
}
340+
324341
__FUNC___run_post_cd_hooks() {
325342
emulate -L zsh
326343
local dir="$1"
@@ -339,9 +356,9 @@ __FUNC___run_post_cd_hooks() {
339356
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
340357
if [ -n "$_gtr_file_hooks" ]; then
341358
# Verify trust before including .gtrconfig hooks
342-
local _gtr_hook_hash
343-
_gtr_hook_hash="$(__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)" || true
344-
if [ -n "$_gtr_hook_hash" ] && [ -f "$_gtr_trust_dir/$_gtr_hook_hash" ]; then
359+
local _gtr_trust_key
360+
_gtr_trust_key="$(__FUNC___hooks_trust_key "$_gtr_config_file" 2>/dev/null)" || true
361+
if [ -n "$_gtr_trust_key" ] && [ -f "$_gtr_trust_dir/$_gtr_trust_key" ]; then
345362
if [ -n "$_gtr_hooks" ]; then
346363
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
347364
else
@@ -528,6 +545,15 @@ function __FUNC___hooks_hash
528545
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
529546
end
530547
548+
function __FUNC___hooks_trust_key
549+
set -l _gtr_config_file "$argv[1]"
550+
set -l _gtr_hook_hash (__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)
551+
test -n "$_gtr_hook_hash"; or return 1
552+
set -l _gtr_repo_root (cd (dirname "$_gtr_config_file") 2>/dev/null; and pwd -P)
553+
test -n "$_gtr_repo_root"; or return 1
554+
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1
555+
end
556+
531557
function __FUNC___run_post_cd_hooks
532558
set -l dir "$argv[1]"
533559
set -l _gtr_trust_dir "$HOME/.config/gtr/trusted"
@@ -547,8 +573,8 @@ function __FUNC___run_post_cd_hooks
547573
set -l _gtr_candidate_hooks (git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)
548574
if test (count $_gtr_candidate_hooks) -gt 0
549575
# Verify trust before including .gtrconfig hooks
550-
set -l _gtr_hook_hash (__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)
551-
if test -n "$_gtr_hook_hash"; and test -f "$_gtr_trust_dir/$_gtr_hook_hash"
576+
set -l _gtr_trust_key (__FUNC___hooks_trust_key "$_gtr_config_file" 2>/dev/null)
577+
if test -n "$_gtr_trust_key"; and test -f "$_gtr_trust_dir/$_gtr_trust_key"
552578
set _gtr_file_hooks $_gtr_candidate_hooks
553579
else
554580
echo "__FUNC__: Untrusted .gtrconfig hooks skipped — run 'git gtr trust' to approve" >&2
@@ -705,6 +731,7 @@ complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a adapter -d 'List avai
705731
complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a config -d 'Manage configuration'
706732
complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a completion -d 'Generate shell completions'
707733
complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a init -d 'Generate shell integration'
734+
complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a trust -d 'Trust .gtrconfig hooks'
708735
complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a version -d 'Show version'
709736
complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a help -d 'Show help'
710737

lib/commands/trust.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,26 @@ cmd_trust() {
2626
return 0
2727
fi
2828

29+
local trust_path
30+
trust_path=$(_hooks_trust_path_for_content "$config_file" "$hook_content") || {
31+
log_error "Failed to compute trust marker for $config_file"
32+
return 1
33+
}
34+
2935
log_warn "The following hooks are defined in $config_file:"
3036
echo "" >&2
3137
printf '%s\n' "$hook_content" >&2
3238
echo "" >&2
3339
log_warn "These commands will execute on your machine during gtr operations."
3440

3541
if prompt_yes_no "Trust these hooks?"; then
36-
if _hooks_mark_trusted "$config_file"; then
42+
if _hooks_write_trust_marker "$trust_path" "$config_file"; then
43+
local current_trust_path
44+
current_trust_path=$(_hooks_trust_path "$config_file") || true
45+
if [ -n "$current_trust_path" ] && [ "$current_trust_path" != "$trust_path" ]; then
46+
log_warn "Hooks changed during review; current hooks remain untrusted"
47+
return 1
48+
fi
3749
log_info "Hooks marked as trusted"
3850
else
3951
log_error "Failed to mark hooks as trusted"

lib/hooks.sh

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,76 @@
66
# user approval before execution. This prevents malicious contributors from
77
# injecting arbitrary commands via shared config files.
88
#
9-
# Trust state is stored per-repo in ~/.config/gtr/trusted/<hash>
10-
# where <hash> is the SHA-256 of the .gtrconfig hooks content.
9+
# Trust state is stored in ~/.config/gtr/trusted/<key>
10+
# where <key> is a SHA-256 of the canonical repo root plus the hook content hash.
1111

1212
_GTR_TRUST_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/gtr/trusted"
1313

14+
# Compute a content hash for hook definitions
15+
# Usage: _hooks_content_hash <hook_content>
16+
_hooks_content_hash() {
17+
local hook_content="$1"
18+
[ -n "$hook_content" ] || return 1
19+
printf '%s\n' "$hook_content" | shasum -a 256 | cut -d' ' -f1
20+
}
21+
1422
# Compute a content hash of all hook entries in a .gtrconfig file
1523
# Usage: _hooks_file_hash <config_file>
1624
_hooks_file_hash() {
1725
local config_file="$1"
1826
local hook_content
1927
hook_content=$(git config -f "$config_file" --get-regexp '^hooks\.' 2>/dev/null) || true
20-
if [ -z "$hook_content" ]; then
21-
return 1
22-
fi
23-
printf '%s\n' "$hook_content" | shasum -a 256 | cut -d' ' -f1
28+
_hooks_content_hash "$hook_content"
29+
}
30+
31+
# Resolve the canonical repository root for a .gtrconfig file
32+
# Usage: _hooks_repo_root <config_file>
33+
_hooks_repo_root() {
34+
local config_file="$1"
35+
(
36+
cd "$(dirname "$config_file")" 2>/dev/null &&
37+
pwd -P
38+
)
39+
}
40+
41+
# Compute the repo-scoped trust key for a .gtrconfig file
42+
# Usage: _hooks_trust_key <config_file>
43+
_hooks_trust_key() {
44+
local config_file="$1"
45+
local hash repo_root
46+
hash=$(_hooks_file_hash "$config_file") || return 1
47+
repo_root=$(_hooks_repo_root "$config_file") || return 1
48+
printf '%s\n%s\n' "$repo_root" "$hash" | shasum -a 256 | cut -d' ' -f1
49+
}
50+
51+
# Compute the repo-scoped trust key for reviewed hook content
52+
# Usage: _hooks_trust_key_for_content <config_file> <hook_content>
53+
_hooks_trust_key_for_content() {
54+
local config_file="$1"
55+
local hook_content="$2"
56+
local hash repo_root
57+
hash=$(_hooks_content_hash "$hook_content") || return 1
58+
repo_root=$(_hooks_repo_root "$config_file") || return 1
59+
printf '%s\n%s\n' "$repo_root" "$hash" | shasum -a 256 | cut -d' ' -f1
2460
}
2561

2662
# Resolve the trust marker path for a .gtrconfig file
2763
# Usage: _hooks_trust_path <config_file>
2864
_hooks_trust_path() {
2965
local config_file="$1"
30-
local hash
31-
hash=$(_hooks_file_hash "$config_file") || return 1
32-
printf '%s/%s\n' "$_GTR_TRUST_DIR" "$hash"
66+
local trust_key
67+
trust_key=$(_hooks_trust_key "$config_file") || return 1
68+
printf '%s/%s\n' "$_GTR_TRUST_DIR" "$trust_key"
69+
}
70+
71+
# Resolve the trust marker path for reviewed hook content
72+
# Usage: _hooks_trust_path_for_content <config_file> <hook_content>
73+
_hooks_trust_path_for_content() {
74+
local config_file="$1"
75+
local hook_content="$2"
76+
local trust_key
77+
trust_key=$(_hooks_trust_key_for_content "$config_file" "$hook_content") || return 1
78+
printf '%s/%s\n' "$_GTR_TRUST_DIR" "$trust_key"
3379
}
3480

3581
# Check if .gtrconfig hooks are trusted for the current repository
@@ -45,15 +91,24 @@ _hooks_are_trusted() {
4591
[ -f "$trust_path" ]
4692
}
4793

94+
# Write a trust marker that matches the reviewed hooks
95+
# Usage: _hooks_write_trust_marker <trust_path> [config_file]
96+
_hooks_write_trust_marker() {
97+
local trust_path="$1"
98+
local config_file="${2:-}"
99+
100+
mkdir -p "$_GTR_TRUST_DIR" || return 1
101+
printf '%s\n' "$config_file" > "$trust_path"
102+
}
103+
48104
# Mark .gtrconfig hooks as trusted
49105
# Usage: _hooks_mark_trusted <config_file>
50106
_hooks_mark_trusted() {
51107
local config_file="$1"
52108
local trust_path
53109
trust_path=$(_hooks_trust_path "$config_file") || return 0
54110

55-
mkdir -p "$_GTR_TRUST_DIR"
56-
printf '%s\n' "$config_file" > "$trust_path"
111+
_hooks_write_trust_marker "$trust_path" "$config_file"
57112
}
58113

59114
# Get hooks, filtering out untrusted .gtrconfig hooks with a warning

tests/adapters.bats

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ EOF
141141
[ "$GTR_AI_CMD_NAME" = "bunx" ]
142142
}
143143

144+
@test "_load_adapter rejects filesystem path commands in generic fallback" {
145+
run _load_adapter "editor" "./bin/gtr" "Editor" "$(_list_registry_names "$_EDITOR_REGISTRY")" "code, vim"
146+
[ "$status" -eq 1 ]
147+
}
148+
144149
@test "_run_configured_command preserves quoted arguments" {
145150
run _run_configured_command "printf '%s\n' 'hello world'"
146151
[ "$status" -eq 0 ]

tests/cmd_trust.bats

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ EOF
3333
EOF
3434

3535
prompt_yes_no() { return 0; }
36-
_hooks_mark_trusted() { return 1; }
36+
_hooks_write_trust_marker() { return 1; }
3737

3838
local output_file="$BATS_TMPDIR/cmd-trust-failure.out"
3939
local rc=0
@@ -46,3 +46,60 @@ EOF
4646
[ "$rc" -eq 1 ]
4747
grep -F "Failed to mark hooks as trusted" "$output_file"
4848
}
49+
50+
@test "cmd_trust trusts the reviewed snapshot and fails if hooks change during confirmation" {
51+
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
52+
[hooks]
53+
postCd = echo reviewed
54+
EOF
55+
56+
local reviewed_trust_path
57+
reviewed_trust_path=$(_hooks_trust_path "$TEST_REPO/.gtrconfig")
58+
59+
prompt_yes_no() {
60+
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
61+
[hooks]
62+
postCd = echo changed
63+
EOF
64+
return 0
65+
}
66+
67+
local output_file="$BATS_TMPDIR/cmd-trust-changed.out"
68+
local rc=0
69+
if cmd_trust >"$output_file" 2>&1; then
70+
rc=0
71+
else
72+
rc=$?
73+
fi
74+
75+
[ "$rc" -eq 1 ]
76+
[ -f "$reviewed_trust_path" ]
77+
! _hooks_are_trusted "$TEST_REPO/.gtrconfig"
78+
grep -F "Hooks changed during review; current hooks remain untrusted" "$output_file"
79+
}
80+
81+
@test "cmd_trust writes the reviewed marker when hooks change during confirmation" {
82+
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
83+
[hooks]
84+
postCd = echo original
85+
EOF
86+
87+
local reviewed_marker
88+
reviewed_marker="$(_hooks_trust_path "$TEST_REPO/.gtrconfig")"
89+
90+
prompt_yes_no() {
91+
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
92+
[hooks]
93+
postCd = echo changed
94+
EOF
95+
return 0
96+
}
97+
98+
run cmd_trust
99+
[ "$status" -eq 1 ]
100+
[[ "$output" == *"Hooks changed during review; current hooks remain untrusted"* ]]
101+
[ -f "$reviewed_marker" ]
102+
103+
run _hooks_are_trusted "$TEST_REPO/.gtrconfig"
104+
[ "$status" -eq 1 ]
105+
}

tests/hooks.bats

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ EOF
4141
_hooks_are_trusted "$TEST_REPO/.gtrconfig"
4242
}
4343

44+
@test "repo-specific trust markers differ for the same hook content" {
45+
local second_repo="$BATS_TMPDIR/second-repo"
46+
mkdir -p "$second_repo"
47+
48+
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
49+
[hooks]
50+
postCd = ./scripts/bootstrap
51+
EOF
52+
53+
cat > "$second_repo/.gtrconfig" <<'EOF'
54+
[hooks]
55+
postCd = ./scripts/bootstrap
56+
EOF
57+
58+
local first_path second_path
59+
first_path="$(_hooks_trust_path "$TEST_REPO/.gtrconfig")"
60+
second_path="$(_hooks_trust_path "$second_repo/.gtrconfig")"
61+
62+
[ "$first_path" != "$second_path" ]
63+
}
64+
4465
@test "run_hooks executes single hook" {
4566
git config --add gtr.hook.postCreate 'touch "$REPO_ROOT/hook-ran"'
4667
run_hooks postCreate REPO_ROOT="$TEST_REPO"

tests/init.bats

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,13 @@ BASH
198198
[[ "$output" == *'printf '\''%s/.gtrconfig\n'\'''* ]]
199199
}
200200

201+
@test "generated wrappers scope trust markers by repo root" {
202+
run cmd_init bash
203+
[ "$status" -eq 0 ]
204+
[[ "$output" == *"hooks_trust_key"* ]]
205+
[[ "$output" == *'dirname "$_gtr_config_file"'* ]]
206+
}
207+
201208
@test "zsh output includes cd completion" {
202209
run cmd_init zsh
203210
[ "$status" -eq 0 ]
@@ -216,6 +223,12 @@ BASH
216223
[[ "$output" == *"-a cd -d"* ]]
217224
}
218225

226+
@test "fish output includes trust subcommand completion" {
227+
run cmd_init fish
228+
[ "$status" -eq 0 ]
229+
[[ "$output" == *"-a trust -d"* ]]
230+
}
231+
219232
@test "fish output uses git gtr list --porcelain for cd completion" {
220233
run cmd_init fish
221234
[ "$status" -eq 0 ]

0 commit comments

Comments
 (0)