Fix: stop the Flutter SDK reading your own repo as itself inside git hooks
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

`git commit`/`git push` export GIT_DIR to every hook they run, and the
flutter tool shells out to git to identify its own SDK. `git -C
"$FLUTTER_ROOT" rev-parse HEAD` in bin/internal/shared.sh does not beat an
inherited GIT_DIR -- `-C` only changes directory, GIT_DIR overrides
repository discovery outright:

    $ git -C /opt/flutter rev-parse HEAD
    058e0af2c2...                                  <- Flutter's HEAD
    $ GIT_DIR=~/diet-guard/.git git -C /opt/flutter rev-parse HEAD
    84bd550331...                                  <- diet-guard's HEAD

So inside a hook, flutter reports the project as the SDK:

    Flutter 3.47.0-0.1.pre • channel main • https://github.com/kuhyx/diet-guard

pub then resolves the SDK as 0.0.0-unknown and every version constraint
becomes unsatisfiable. flutter also writes that into its own version cache,
so the wrong answer outlives the hook and follows the next caller into an
unrelated repository -- which is why the identical command passes when you
re-run it by hand and fails again on the next push.

diet-guard's pre-push gate hit this today. Clearing the variables per call in
each hook works, and screen-locker and diet-guard now do, but only for as
long as every future hook author remembers -- deployment hygiene, not a fix.
This installs a wrapper ahead of /usr/bin on PATH that strips them for every
caller in every repo, covering both flutter and dart. It removes its own
directory from PATH before exec so the Flutter entry point's internal `which
flutter` cannot loop back into it, and it verifies itself by running flutter
against a throwaway poisoned GIT_DIR and failing unless the real SDK comes
back.

Not patching bin/internal/shared.sh: that file belongs to the flutter-bin
package, so the next update would silently restore the bug. Upstream already
unsets these variables in content_aware_hash.sh and update_engine_version.sh,
just not in the version path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-07-26 15:39:33 +02:00
parent d91c0ab0e9
commit 071963b92b
2 changed files with 241 additions and 0 deletions

View File

@ -0,0 +1,58 @@
{
"intent": "Make it impossible for the Flutter SDK to misidentify itself when invoked from a git hook, so Flutter gates in any repo stop failing with 'The current Flutter SDK version is 0.0.0-unknown'.",
"scope": [
"linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh (new)",
"Installs to ~/.local/share/flutter-git-env/wrapper.sh with ~/.local/bin/{flutter,dart} symlinks",
"Non-goal: patching /opt/flutter/bin/internal/shared.sh — pacman-owned by flutter-bin, an update would silently restore the bug",
"Non-goal: CI runners, which do not set GIT_DIR because they call flutter from workflow steps rather than hooks"
],
"changes": [
"Root cause: git exports GIT_DIR to its hooks, and `git -C \"$FLUTTER_ROOT\" rev-parse HEAD` in shared.sh does not beat it — `-C` only changes directory while GIT_DIR overrides repository discovery. flutter therefore reads the repo being committed as its own SDK, reports that repo's HEAD as the framework revision, resolves the SDK as 0.0.0-unknown, and caches the wrong answer in bin/cache/flutter.version.json where it poisons later callers in other repos.",
"Adds a PATH wrapper ahead of /usr/bin that strips GIT_DIR and the other git hook variables, then execs the real binary. Covers flutter and dart (both are aur_* shims into /opt/flutter).",
"The wrapper removes its own directory from PATH before exec, so the Flutter entry point's internal `which flutter` lookup cannot loop back into it.",
"Ships --check (re-verify, e.g. after a flutter update) and --uninstall. Install is idempotent.",
"Verification is built in: the installer creates a throwaway git repo, runs flutter with GIT_DIR pointed at it, and fails unless flutter still reports github.com/flutter/flutter."
],
"verification": [
{
"command": "GIT_DIR=/home/kuhy/diet-guard/.git git -C /opt/flutter rev-parse HEAD",
"result": "reproduced",
"evidence": "84bd5503316568914ed93b9451c397ff517a09d4 (diet-guard's HEAD) vs 058e0af2c2... without GIT_DIR — confirms `git -C` loses to GIT_DIR"
},
{
"command": "GIT_DIR=/home/kuhy/diet-guard/.git flutter --version # before the fix",
"result": "reproduced",
"evidence": "Flutter 3.47.0-0.1.pre • channel main • https://github.com/kuhyx/diet-guard — the SDK reporting the project as itself"
},
{
"command": "cd /home/kuhy/diet-guard/app && GIT_DIR=/home/kuhy/diet-guard/.git flutter analyze # after the fix, no per-call env -u",
"result": "pass",
"evidence": "Analyzing app... No issues found! (ran in 1.2s) — previously failed with 'url_launcher_platform_interface ^2.3.2 is forbidden' against 0.0.0-unknown"
},
{
"command": "GIT_DIR=... dart --version; flutter --version; command -v flutter dart",
"result": "pass",
"evidence": "Dart SDK version: 3.12.2 (stable); Flutter 3.44.8 • channel stable • github.com/flutter/flutter; both resolve to /home/kuhy/.local/bin — normal use unaffected, no exec loop"
},
{
"command": "bash fix_flutter_git_env.sh --check && bash fix_flutter_git_env.sh",
"result": "pass",
"evidence": "verified: Flutter 3.44.8 • channel stable • https://github.com/flutter/flutter.git; reinstall is idempotent"
},
{
"command": "shellcheck linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh",
"result": "pass",
"evidence": "no findings"
}
],
"risks": [
"Shadows /usr/bin/flutter and /usr/bin/dart on PATH for this user. Mitigated by exec'ing the real binary by absolute path and by the installer refusing to overwrite a non-symlink at either location.",
"If ~/.local/bin ever stops being on PATH the wrapper silently stops applying; the installer and --check both fail loudly on that, but only when run.",
"Per-call `env -u GIT_DIR` remains in screen-locker's and diet-guard's hooks on purpose: those repos may be cloned onto machines without this wrapper."
],
"rollback": [
"bash linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh --uninstall",
"Confirm `command -v flutter` is /usr/bin/flutter again and `flutter --version` reports 3.x on channel stable",
"Expect Flutter gates run from git hooks to start failing with 0.0.0-unknown again unless the per-call env -u fixes are present"
]
}

View File

@ -0,0 +1,183 @@
#!/bin/bash
# ============================================================================
# Stop the Flutter SDK misreading YOUR repository as itself inside git hooks.
#
# `git commit` / `git push` export GIT_DIR (and GIT_WORK_TREE,
# GIT_INDEX_FILE) to every hook they run. The flutter tool shells out to git
# to identify its own SDK, and `git -C "$FLUTTER_ROOT" rev-parse HEAD`
# (bin/internal/shared.sh) does NOT beat an inherited GIT_DIR -- `-C` only
# changes directory, while GIT_DIR overrides repository discovery outright:
#
# $ git -C /opt/flutter rev-parse HEAD
# 058e0af2c2... <- Flutter's HEAD
# $ GIT_DIR=~/some-repo/.git git -C /opt/flutter rev-parse HEAD
# 84bd550331... <- YOUR repo's HEAD
#
# So inside a hook, `flutter --version` reports your project as the SDK, pub
# resolves the SDK as "0.0.0-unknown" and every version constraint becomes
# unsatisfiable ("... is forbidden"). Worse, flutter caches that answer in
# bin/cache/flutter.version.json, so the corruption outlives the hook and
# follows the next caller into an unrelated repository -- which is why the
# same command passes when you re-run it by hand and fails again next push.
#
# Upstream already unsets these variables in bin/internal/content_aware_hash.sh
# and bin/internal/update_engine_version.sh, but not in the version path.
#
# THE FIX: a wrapper ahead of /usr/bin on PATH that strips the git-hook
# variables and hands off to the real binary. Per-call `env -u GIT_DIR ...` in
# each hook works too, but only for as long as every future hook author
# remembers; this makes the failure impossible for every caller in every repo.
#
# Deliberately NOT patching /opt/flutter/bin/internal/shared.sh: that file is
# owned by the flutter-bin package and the next update would silently restore
# the bug.
#
# Usage: fix_flutter_git_env.sh [--check | --uninstall]
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
readonly BIN_DIR="${HOME}/.local/bin"
readonly LIB_DIR="${HOME}/.local/share/flutter-git-env"
readonly WRAPPER="${LIB_DIR}/wrapper.sh"
# Every SDK entry point that shells out to git to identify itself.
readonly TOOLS=(flutter dart)
log() { echo "$SCRIPT_NAME: $*"; }
fail() {
echo "$SCRIPT_NAME: $*" >&2
exit 1
}
write_wrapper() {
mkdir -p "$LIB_DIR" "$BIN_DIR"
cat > "$WRAPPER" <<'WRAPPER_EOF'
#!/bin/bash
# Strip the git-hook environment, then hand off to the real SDK binary.
# Installed by linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh
# -- see that script for why this exists. Invoked through a symlink named
# after the tool it fronts (flutter, dart).
set -euo pipefail
tool="$(basename "$0")"
real="/usr/bin/${tool}"
[[ -x $real ]] || {
echo "flutter-git-env: $real is missing or not executable" >&2
exit 127
}
# Drop our own directory so a PATH lookup inside the real tool cannot find
# this wrapper again. The Flutter entry point does exactly such a lookup.
clean_path=""
IFS=':' read -r -a path_parts <<< "$PATH"
for part in "${path_parts[@]}"; do
[[ $part == "$HOME/.local/bin" ]] && continue
clean_path="${clean_path:+$clean_path:}$part"
done
# GIT_DIR is the one that actually bites; the rest are exported by git in the
# same breath and would mislead the SDK the same way.
exec env \
-u GIT_DIR \
-u GIT_WORK_TREE \
-u GIT_INDEX_FILE \
-u GIT_COMMON_DIR \
-u GIT_OBJECT_DIRECTORY \
-u GIT_ALTERNATE_OBJECT_DIRECTORIES \
-u GIT_NAMESPACE \
-u GIT_PREFIX \
PATH="$clean_path" \
"$real" "$@"
WRAPPER_EOF
chmod +x "$WRAPPER"
}
link_tools() {
local tool link
for tool in "${TOOLS[@]}"; do
link="${BIN_DIR}/${tool}"
if [[ -e $link && ! -L $link ]]; then
fail "$link exists and is not a symlink — refusing to overwrite it"
fi
ln -sfn "$WRAPPER" "$link"
log "linked $link -> $WRAPPER"
done
}
check_path() {
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) fail "$BIN_DIR is not on PATH — the wrapper would never be used" ;;
esac
local resolved
for tool in "${TOOLS[@]}"; do
resolved="$(command -v "$tool" || true)"
[[ $resolved == "${BIN_DIR}/${tool}" ]] ||
fail "$tool resolves to '$resolved', not the wrapper — check PATH order"
done
}
# The real test: does the SDK still know what it is with GIT_DIR poisoned?
verify() {
local probe_repo probe_version
probe_repo="$(mktemp -d)"
git -C "$probe_repo" init -q
git -C "$probe_repo" -c user.email=x@y -c user.name=x commit -q --allow-empty -m probe
probe_version="$(GIT_DIR="$probe_repo/.git" flutter --version 2>&1 | head -1 || true)"
rm -rf "$probe_repo"
if grep -q '0\.0\.0-unknown' <<< "$probe_version"; then
fail "still broken: flutter reports an unknown SDK under GIT_DIR"
fi
if ! grep -q 'flutter/flutter' <<< "$probe_version"; then
fail "still broken: flutter reports '$probe_version' under GIT_DIR"
fi
log "verified: $probe_version"
}
uninstall() {
local tool link
for tool in "${TOOLS[@]}"; do
link="${BIN_DIR}/${tool}"
if [[ -L $link && "$(readlink -f "$link")" == "$WRAPPER" ]]; then
rm -f "$link"
log "removed $link"
fi
done
rm -rf "$LIB_DIR"
log "uninstalled"
}
main() {
case "${1:-}" in
--uninstall)
uninstall
return
;;
--check)
[[ -x $WRAPPER ]] || fail "wrapper is not installed"
check_path
verify
return
;;
"") ;;
*) fail "unknown option: $1 (expected --check or --uninstall)" ;;
esac
command -v flutter >/dev/null 2>&1 || fail "flutter is not on PATH"
write_wrapper
link_tools
check_path
verify
log "done — flutter/dart now ignore an inherited GIT_DIR"
}
main "$@"