From 12dd7937233f19bddd12456162b84f8ec8ee80de Mon Sep 17 00:00:00 2001 From: Shi Qi Date: Wed, 4 Nov 2026 09:56:53 -0800 Subject: [PATCH 1/6] test: document and cover Git configuration trust policy --- docs/configuration.md | 12 +++ tests/config_trust_policy.bats | 132 +++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/config_trust_policy.bats diff --git a/docs/configuration.md b/docs/configuration.md index 71cad9d..c912c2a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -37,6 +37,18 @@ All configuration is stored via `git config`, making it easy to manage per-repos 5. Environment variables 6. Default values +Git configuration is a trusted input: GTR runs hooks from local, global, and +system Git configuration without a separate `git gtr trust` approval. Anyone +who can modify that configuration can affect commands run by both GTR and Git +itself. Keep `.git/config` and any files it includes writable only by people you +trust to run commands as your user. The `.gtrconfig` approval gate does not +isolate users sharing writable Git configuration. + +`git gtr trust` approves executable `.gtrconfig` entries (hooks and editor/AI +defaults). Other settings follow the precedence above without that approval. +For example, `worktrees.dir` selects the worktree destination; honoring this +path does not approve `.gtrconfig` hooks. + For the CLI, a bare repository's root is the bare Git directory itself, including when a command is run from one of its linked worktrees. `.gtrconfig`, `.worktreeinclude`, relative worktree locations, and the default copy source use diff --git a/tests/config_trust_policy.bats b/tests/config_trust_policy.bats new file mode 100644 index 0000000..2f50c90 --- /dev/null +++ b/tests/config_trust_policy.bats @@ -0,0 +1,132 @@ +#!/usr/bin/env bats +# Git configuration is user-controlled; .gtrconfig commands require approval. +# Fixture commands expand variables only when invoked by the child shell. +# BATS owns the status/output variables across its test functions. +# shellcheck disable=SC2016,SC2030,SC2031 +load test_helper + +setup() { + POLICY_FIXTURE=$(cd -P "$(mktemp -d)" && pwd) + export XDG_CONFIG_HOME="$POLICY_FIXTURE/config" + export XDG_CACHE_HOME="$POLICY_FIXTURE/cache" + export XDG_DATA_HOME="$POLICY_FIXTURE/data" + export GIT_CONFIG_GLOBAL="$POLICY_FIXTURE/global.gitconfig" + export GIT_CONFIG_NOSYSTEM=1 + export POLICY_EVENTS="$POLICY_FIXTURE/events" + : > "$GIT_CONFIG_GLOBAL" + setup_integration_repo + source_gtr_commands +} + +teardown() { + teardown_integration_repo + rm -rf "$POLICY_FIXTURE" +} + +# Write globals by explicit filename so fixture setup cannot modify user config. +configure_policy_hooks() { + local phase="$1" + git config -f "$GIT_CONFIG_GLOBAL" --add "gtr.hook.$phase" \ + "printf '%s\\n' 'global.$phase' >> \"\$POLICY_EVENTS\"" + git config --local --add "gtr.hook.$phase" \ + "printf '%s\\n' 'local.$phase' >> \"\$POLICY_EVENTS\"" + git config -f "$TEST_REPO/.gtrconfig" --add "hooks.$phase" \ + "printf '%s\\n' 'untrusted.$phase' >> \"\$POLICY_EVENTS\"" +} + +check_policy_lifecycle() { + local scope="$1" destination="$POLICY_FIXTURE/worktrees with spaces" + local phase result + for phase in postCreate preRemove postRemove; do + configure_policy_hooks "$phase" + done + git config -f "$GIT_CONFIG_GLOBAL" gtr.worktrees.dir "$destination" + if [ "$scope" = local ]; then + # Local configuration must override both team and global destinations. + git config -f "$GIT_CONFIG_GLOBAL" gtr.worktrees.dir "$POLICY_FIXTURE/global-unused" + git config -f "$TEST_REPO/.gtrconfig" worktrees.dir "$POLICY_FIXTURE/team-unused" + git config --local gtr.worktrees.dir "$destination" + fi + + # Use the actual CLI (set -e), with all network operations disabled. + result=$("$PROJECT_ROOT/bin/gtr" new policy-feature --from HEAD --no-fetch --no-copy \ + --porcelain 2> "$POLICY_FIXTURE/create.stderr") + [ "$result" = $'path\t'"$destination/policy-feature"$'\nbranch\tpolicy-feature\nhook_status\tpartial' ] + [ -d "$destination/policy-feature" ] + [ ! -e "$POLICY_FIXTURE/global-unused" ] + [ ! -e "$POLICY_FIXTURE/team-unused" ] + [ "$(cat "$POLICY_EVENTS")" = $'global.postCreate\nlocal.postCreate' ] + + run "$PROJECT_ROOT/bin/gtr" rm policy-feature --yes + [ "$status" -eq 0 ] + [ ! -e "$destination/policy-feature" ] + [ "$(cat "$POLICY_EVENTS")" = $'global.postCreate\nlocal.postCreate\nglobal.preRemove\nlocal.preRemove\nglobal.postRemove\nlocal.postRemove' ] + [ ! -d "$_GTR_TRUST_DIR" ] +} + +@test "local worktrees.dir and Git lifecycle hooks work while untrusted file hooks stay blocked" { + check_policy_lifecycle local +} + +@test "global worktrees.dir and Git lifecycle hooks work while untrusted file hooks stay blocked" { + check_policy_lifecycle global +} + +@test "untrusted worktrees.dir selects a literal destination without approving file hooks" { + local destination="$POLICY_FIXTURE/"'$(touch should-not-run)' + git config -f "$TEST_REPO/.gtrconfig" worktrees.dir "$destination" + git config -f "$TEST_REPO/.gtrconfig" hooks.postCreate 'printf untrusted > "$POLICY_EVENTS"' + + run "$PROJECT_ROOT/bin/gtr" new literal-path --from HEAD --no-fetch --no-copy --porcelain + [ "$status" -eq 0 ] + [[ "$output" == *$'hook_status\tskipped-untrusted'* ]] + [ -d "$destination/literal-path" ] + [ ! -e "$TEST_REPO/should-not-run" ] + [ ! -e "$POLICY_EVENTS" ] + [ ! -d "$_GTR_TRUST_DIR" ] +} + +@test "postCd export dispatcher runs Git hooks while untrusted file hooks stay blocked" { + configure_policy_hooks postCd + git config --local --add gtr.hook.postCd 'export POLICY_LOCAL_EXPORT=local' + git config -f "$TEST_REPO/.gtrconfig" --add hooks.postCd 'export POLICY_LOCAL_EXPORT=untrusted' + + # Run with strict mode in a child shell, as used by the AI launch path. + run bash -e -c ' + source "$GTR_DIR/lib/config.sh" + source "$GTR_DIR/lib/hooks.sh" + run_hooks_export postCd + [ "$POLICY_LOCAL_EXPORT" = local ] + ' + [ "$status" -eq 0 ] + [ "$(cat "$POLICY_EVENTS")" = $'global.postCd\nlocal.postCd' ] + [ ! -d "$_GTR_TRUST_DIR" ] +} + +check_generated_policy_hooks() { + local shell_name="$1" + command -v "$shell_name" >/dev/null 2>&1 || skip "$shell_name is not installed" + configure_policy_hooks postCd + export POLICY_INIT="$POLICY_FIXTURE/init.$shell_name" + export POLICY_WORKTREE="$POLICY_FIXTURE/linked worktree" + git worktree add --quiet "$POLICY_WORKTREE" -b policy-cd HEAD + "$PROJECT_ROOT/bin/gtr" init "$shell_name" > "$POLICY_INIT" + + run "$shell_name" -c 'source "$POLICY_INIT"; gtr_run_post_cd_hooks "$POLICY_WORKTREE"' + [ "$status" -eq 0 ] + [[ "$output" == *"Untrusted .gtrconfig hooks skipped"* ]] + [ "$(cat "$POLICY_EVENTS")" = $'global.postCd\nlocal.postCd' ] + [ ! -d "$_GTR_TRUST_DIR" ] +} + +@test "bash generated postCd runs Git hooks while untrusted file hooks stay blocked" { + check_generated_policy_hooks bash +} + +@test "zsh generated postCd runs Git hooks while untrusted file hooks stay blocked" { + check_generated_policy_hooks zsh +} + +@test "fish generated postCd runs Git hooks while untrusted file hooks stay blocked" { + check_generated_policy_hooks fish +} From fac1484cd1000677aaefd663f36eaeb50f4fdc63 Mon Sep 17 00:00:00 2001 From: Shi Qi Date: Wed, 4 Nov 2026 10:11:14 -0800 Subject: [PATCH 2/6] test: address trust policy review feedback --- docs/configuration.md | 11 ++++++-- tests/config_trust_policy.bats | 48 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c912c2a..436b551 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -28,7 +28,7 @@ All configuration is stored via `git config`, making it easy to manage per-repository or globally. You can also use a `.gtrconfig` file for team-shared settings. -**Configuration precedence** (highest to lowest): +**Configuration precedence for single-value settings** (highest to lowest): 1. `git config --local` (`.git/config`) - personal overrides 2. `.gtrconfig` (repo root) - team defaults @@ -37,6 +37,12 @@ All configuration is stored via `git config`, making it easy to manage per-repos 5. Environment variables 6. Default values +Multi-valued copy settings merge values from local, `.gtrconfig`, global, and +system configuration, removing duplicates. For example, a local +`gtr.copy.includeDirs=vendor` and `.gtrconfig` `copy.includeDirs=node_modules` +can cause both directories to be copied; the local value does not replace the +team value. + Git configuration is a trusted input: GTR runs hooks from local, global, and system Git configuration without a separate `git gtr trust` approval. Anyone who can modify that configuration can affect commands run by both GTR and Git @@ -45,7 +51,8 @@ trust to run commands as your user. The `.gtrconfig` approval gate does not isolate users sharing writable Git configuration. `git gtr trust` approves executable `.gtrconfig` entries (hooks and editor/AI -defaults). Other settings follow the precedence above without that approval. +defaults). Other settings use the single-value precedence or multi-value merging +described above without that approval. For example, `worktrees.dir` selects the worktree destination; honoring this path does not approve `.gtrconfig` hooks. diff --git a/tests/config_trust_policy.bats b/tests/config_trust_policy.bats index 2f50c90..648b7fd 100644 --- a/tests/config_trust_policy.bats +++ b/tests/config_trust_policy.bats @@ -5,6 +5,7 @@ # shellcheck disable=SC2016,SC2030,SC2031 load test_helper +# Create an isolated repository and configuration/cache paths for each test. setup() { POLICY_FIXTURE=$(cd -P "$(mktemp -d)" && pwd) export XDG_CONFIG_HOME="$POLICY_FIXTURE/config" @@ -18,12 +19,15 @@ setup() { source_gtr_commands } +# Remove registered worktrees before deleting the test's configuration fixtures. teardown() { teardown_integration_repo rm -rf "$POLICY_FIXTURE" } -# Write globals by explicit filename so fixture setup cannot modify user config. +# Configure marker hooks for a phase in Git config and unapproved .gtrconfig. +# Usage: configure_policy_hooks +# Writes globals by explicit filename so setup cannot modify user config. configure_policy_hooks() { local phase="$1" git config -f "$GIT_CONFIG_GLOBAL" --add "gtr.hook.$phase" \ @@ -34,6 +38,9 @@ configure_policy_hooks() { "printf '%s\\n' 'untrusted.$phase' >> \"\$POLICY_EVENTS\"" } +# Verify destination precedence and hook trust across real CLI create/remove. +# Usage: check_policy_lifecycle +# Asserts that Git hooks run, file hooks stay blocked, and no approval is stored. check_policy_lifecycle() { local scope="$1" destination="$POLICY_FIXTURE/worktrees with spaces" local phase result @@ -103,16 +110,53 @@ check_policy_lifecycle() { [ ! -d "$_GTR_TRUST_DIR" ] } +# Verify a generated shell helper preserves only trusted hooks and exports. +# Usage: check_generated_policy_hooks +# Skips unavailable shells; assertion failures propagate to the BATS test. check_generated_policy_hooks() { local shell_name="$1" command -v "$shell_name" >/dev/null 2>&1 || skip "$shell_name is not installed" configure_policy_hooks postCd + local trusted_export_hook untrusted_export_hook + case "$shell_name" in + bash|zsh) + trusted_export_hook='export POLICY_TRUSTED_EXPORT=trusted' + untrusted_export_hook='export POLICY_UNTRUSTED_EXPORT=untrusted' + ;; + fish) + trusted_export_hook='set -gx POLICY_TRUSTED_EXPORT trusted' + untrusted_export_hook='set -gx POLICY_UNTRUSTED_EXPORT untrusted' + ;; + esac + git config --local --add gtr.hook.postCd "$trusted_export_hook" + git config -f "$TEST_REPO/.gtrconfig" --add hooks.postCd "$untrusted_export_hook" export POLICY_INIT="$POLICY_FIXTURE/init.$shell_name" export POLICY_WORKTREE="$POLICY_FIXTURE/linked worktree" git worktree add --quiet "$POLICY_WORKTREE" -b policy-cd HEAD "$PROJECT_ROOT/bin/gtr" init "$shell_name" > "$POLICY_INIT" - run "$shell_name" -c 'source "$POLICY_INIT"; gtr_run_post_cd_hooks "$POLICY_WORKTREE"' + unset POLICY_TRUSTED_EXPORT POLICY_UNTRUSTED_EXPORT + case "$shell_name" in + bash|zsh) + run "$shell_name" -c ' + set -e + # Completion registration is outside this non-interactive hook fixture. + compdef() { :; } + source "$POLICY_INIT" + gtr_run_post_cd_hooks "$POLICY_WORKTREE" + [ "$POLICY_TRUSTED_EXPORT" = trusted ] + [ -z "${POLICY_UNTRUSTED_EXPORT+x}" ] + ' + ;; + fish) + run fish -c ' + source "$POLICY_INIT"; or exit 1 + gtr_run_post_cd_hooks "$POLICY_WORKTREE"; or exit 1 + test "$POLICY_TRUSTED_EXPORT" = trusted; or exit 1 + not set -q POLICY_UNTRUSTED_EXPORT + ' + ;; + esac [ "$status" -eq 0 ] [[ "$output" == *"Untrusted .gtrconfig hooks skipped"* ]] [ "$(cat "$POLICY_EVENTS")" = $'global.postCd\nlocal.postCd' ] From 4deb3dfbfc7f3aba4d65fb9191a791171a47cbc9 Mon Sep 17 00:00:00 2001 From: Shi Qi Date: Wed, 4 Nov 2026 10:53:50 -0800 Subject: [PATCH 3/6] docs: clarify color precedence and isolate zsh tests --- docs/configuration.md | 16 +++++++++++----- tests/config_trust_policy.bats | 10 +++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 436b551..0cb7d7d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -51,8 +51,10 @@ trust to run commands as your user. The `.gtrconfig` approval gate does not isolate users sharing writable Git configuration. `git gtr trust` approves executable `.gtrconfig` entries (hooks and editor/AI -defaults). Other settings use the single-value precedence or multi-value merging -described above without that approval. +defaults). Other settings do not require that approval. Most single-value +settings use the precedence above, and multi-valued copy settings merge across +scopes. UI color is an exception: `.gtrconfig` `ui.color` is not read, and a +nonempty `NO_COLOR` overrides Git configuration (see [UI Settings](#ui-settings)). For example, `worktrees.dir` selects the worktree destination; honoring this path does not approve `.gtrconfig` hooks. @@ -488,7 +490,7 @@ Control color output behavior. | Git Config Key | `.gtrconfig` Key | Description | Values | | -------------- | ---------------- | ----------------- | ----------------------------------- | -| `gtr.ui.color` | `ui.color` | Color output mode | `auto` (default), `always`, `never` | +| `gtr.ui.color` | Not read | Color output mode | `auto` (default), `always`, `never` | ```bash # Disable color output @@ -498,9 +500,13 @@ git gtr config set gtr.ui.color never git gtr config set gtr.ui.color always ``` -**Precedence**: `NO_COLOR` env (highest) > `GTR_COLOR` env > `gtr.ui.color` config > auto-detect (TTY). +Color is initialized from `GTR_COLOR`: `always` enables it, `never` disables it, +and `auto` uses TTY detection. Then `gtr.ui.color` is read using Git's native +configuration precedence, without reading `.gtrconfig`. A value of `always` or +`never` overrides the initial mode; `auto` leaves the initial mode unchanged. -The `NO_COLOR` environment variable ([no-color.org](https://no-color.org)) always wins regardless of other settings. +A nonempty `NO_COLOR` environment variable ([no-color.org](https://no-color.org)) +disables color regardless of either source. --- diff --git a/tests/config_trust_policy.bats b/tests/config_trust_policy.bats index 648b7fd..1a1591b 100644 --- a/tests/config_trust_policy.bats +++ b/tests/config_trust_policy.bats @@ -136,9 +136,13 @@ check_generated_policy_hooks() { "$PROJECT_ROOT/bin/gtr" init "$shell_name" > "$POLICY_INIT" unset POLICY_TRUSTED_EXPORT POLICY_UNTRUSTED_EXPORT + local shell_args=() + if [ "$shell_name" = zsh ]; then + shell_args=(-f) + fi case "$shell_name" in bash|zsh) - run "$shell_name" -c ' + run "$shell_name" "${shell_args[@]}" -c ' set -e # Completion registration is outside this non-interactive hook fixture. compdef() { :; } @@ -168,6 +172,10 @@ check_generated_policy_hooks() { } @test "zsh generated postCd runs Git hooks while untrusted file hooks stay blocked" { + # The helper shell must skip user startup files that could replace fixture config. + export ZDOTDIR="$POLICY_FIXTURE/zsh-startup" + mkdir -p "$ZDOTDIR" + printf 'exit 99\n' > "$ZDOTDIR/.zshenv" check_generated_policy_hooks zsh } From c70cd38a994a9b248f494143eaa9d569f5aba8c4 Mon Sep 17 00:00:00 2001 From: Shi Qi Date: Wed, 4 Nov 2026 11:15:41 -0800 Subject: [PATCH 4/6] docs: distinguish color display from runtime behavior --- docs/configuration.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0cb7d7d..e8e9c02 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -53,8 +53,9 @@ isolate users sharing writable Git configuration. `git gtr trust` approves executable `.gtrconfig` entries (hooks and editor/AI defaults). Other settings do not require that approval. Most single-value settings use the precedence above, and multi-valued copy settings merge across -scopes. UI color is an exception: `.gtrconfig` `ui.color` is not read, and a -nonempty `NO_COLOR` overrides Git configuration (see [UI Settings](#ui-settings)). +scopes. UI color is an exception: `.gtrconfig` `ui.color` is not used for runtime +color, and a nonempty `NO_COLOR` overrides Git configuration +(see [UI Settings](#ui-settings)). For example, `worktrees.dir` selects the worktree destination; honoring this path does not approve `.gtrconfig` hooks. @@ -490,7 +491,7 @@ Control color output behavior. | Git Config Key | `.gtrconfig` Key | Description | Values | | -------------- | ---------------- | ----------------- | ----------------------------------- | -| `gtr.ui.color` | Not read | Color output mode | `auto` (default), `always`, `never` | +| `gtr.ui.color` | `ui.color` (not used for runtime color) | Color output mode | `auto` (default), `always`, `never` | ```bash # Disable color output @@ -502,8 +503,10 @@ git gtr config set gtr.ui.color always Color is initialized from `GTR_COLOR`: `always` enables it, `never` disables it, and `auto` uses TTY detection. Then `gtr.ui.color` is read using Git's native -configuration precedence, without reading `.gtrconfig`. A value of `always` or -`never` overrides the initial mode; `auto` leaves the initial mode unchanged. +configuration precedence. A value of `always` or `never` overrides the initial +mode; `auto` leaves the initial mode unchanged. The `.gtrconfig` value is not +used for runtime color, but `git gtr config get gtr.ui.color` and +`git gtr config list` can still display it. A nonempty `NO_COLOR` environment variable ([no-color.org](https://no-color.org)) disables color regardless of either source. From 12439d016c4be628a7ff386e777f0186925de050 Mon Sep 17 00:00:00 2001 From: Shi Qi Date: Wed, 4 Nov 2026 11:27:00 -0800 Subject: [PATCH 5/6] docs: cover permissions for every Git configuration scope --- docs/configuration.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e8e9c02..133aaa7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -46,9 +46,10 @@ team value. Git configuration is a trusted input: GTR runs hooks from local, global, and system Git configuration without a separate `git gtr trust` approval. Anyone who can modify that configuration can affect commands run by both GTR and Git -itself. Keep `.git/config` and any files it includes writable only by people you -trust to run commands as your user. The `.gtrconfig` approval gate does not -isolate users sharing writable Git configuration. +itself. Keep local, global, and system Git configuration files, and any files +they include, writable only by people you trust to run commands as your user. +The `.gtrconfig` approval gate does not isolate users sharing writable Git +configuration. `git gtr trust` approves executable `.gtrconfig` entries (hooks and editor/AI defaults). Other settings do not require that approval. Most single-value From 38c884a8a26b19b8e0e120dc3aab75c7370ee052 Mon Sep 17 00:00:00 2001 From: Shi Qi Date: Wed, 4 Nov 2026 11:31:39 -0800 Subject: [PATCH 6/6] docs: clarify protection against configuration replacement --- docs/configuration.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 133aaa7..be29785 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -46,10 +46,11 @@ team value. Git configuration is a trusted input: GTR runs hooks from local, global, and system Git configuration without a separate `git gtr trust` approval. Anyone who can modify that configuration can affect commands run by both GTR and Git -itself. Keep local, global, and system Git configuration files, and any files -they include, writable only by people you trust to run commands as your user. -The `.gtrconfig` approval gate does not isolate users sharing writable Git -configuration. +itself. Keep local, global, and system Git configuration files, their included +files, and any symlink targets writable only by people you trust to run commands +as your user. Protect their parent directories as well, so untrusted users +cannot replace files or links. The `.gtrconfig` approval gate does not isolate +users sharing writable Git configuration. `git gtr trust` approves executable `.gtrconfig` entries (hooks and editor/AI defaults). Other settings do not require that approval. Most single-value