From 9901ec5da021c6253f651ba707c1b08faba54c5f Mon Sep 17 00:00:00 2001 From: bchanot Date: Wed, 7 Oct 2026 18:22:34 +0200 Subject: [PATCH 1/4] feat(install): ask the gitflow push mode at install, fixed hooksPath, identity into gitconfig gitconfig template: [gitflow] autopush = @AUTOPUSH@ (exact true/false, the hooks fail closed on anything else) and a fixed core.hooksPath = ~/.claude/githooks (created by `make link` in claude-config, git expands ~). install.sh: resolve_autopush at the identity step. A true/false already in ~/.gitconfig wins silently, else DOTFILES_GITFLOW_AUTOPUSH (any other value aborts before a file is touched), else a prompt that re-asks until the answer is exactly true or false (Enter = true), else true. render_gitconfig refuses to write when the mode is not a boolean or @AUTOPUSH@ survives. Fix: deploy_gitconfig received the repo bashrc template, so every install wrote `name = @USER@` / `email = @EMAIL@`. It now takes the resolved identity directly. patsub_replacement is turned off so an `&` in a name is not expanded on bash >= 5.2. --- README.md | 6 ++-- gitconfig | 14 +++++++--- install.sh | 81 ++++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9765625..0f4342f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install. | `vim/vimrc` | Vim config: pathogen, molokai, syntastic (C with `-Wall -Werror -Wextra`), NERDTree, 42-style canonical class generators (`:ClassH`, `:ClassC`). | | `vim/autoload/` | `pathogen.vim` plugin loader (committed). | | `vim/colors/` | `molokai.vim` colorscheme (committed). | -| `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@` and `@EMAIL@` are filled at install with the `USER` and `EMAIL` exported by the bashrc (git never expands `$VARS` itself). | +| `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@`, `@EMAIL@` and `@AUTOPUSH@` (gitflow push mode) are filled at install with the installer's answers (git never expands `$VARS` itself); `core.hooksPath` points at the gitflow hooks `make link` creates. | | `bash/bashrc-linux` | bashrc for desktop Linux (git-aware prompt + command timer). | | `bash/bashrc-osx` | bashrc for macOS: `bashrc-linux` adapted (Homebrew on `PATH`, BSD `ls -G`, bash 5 clock for the timer, `cc` without `systemd-run`). | | `zsh/zshrc-osx` | zshrc for macOS when zsh is chosen: oh-my-zsh + the same env, aliases and dtach menu as `bashrc-osx`. Loads `~/.zshrc.local` for machine-specific lines. | @@ -47,7 +47,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install. curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install.sh | bash ``` -`remote-install.sh` ensures `git` is present, clones the repo to `~/config` (or pulls if already there), then runs `install.sh` with the terminal as its stdin, so the questions below (identity, macOS shell, offers) are asked even though the script arrives through a pipe. Override with env vars: `REPO_URL=... CLONE_DIR=... BRANCH=... curl ... | bash`. +`remote-install.sh` ensures `git` is present, clones the repo to `~/config` (or pulls if already there), then runs `install.sh` with the terminal as its stdin, so the questions below (identity, push mode, macOS shell, offers) are asked even though the script arrives through a pipe. Override with env vars: `REPO_URL=... CLONE_DIR=... BRANCH=... curl ... | bash`. > Piping a remote script into `bash` runs unreviewed code over the network. Read [`remote-install.sh`](remote-install.sh) first, or use the manual clone below. @@ -67,7 +67,7 @@ What it does: 3. Moves any existing `~/.vim`, `~/.vimrc`, `~/.bashrc`, `~/.Sublivim` to `~/Oldconfig`. 4. Clones the `syntastic` and `nerdtree` vim plugins into `~/.vim/bundle/`. 5. Copies the tracked vim files into `~/.vim` and symlinks `~/.vimrc`. -6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same `USER` / `EMAIL`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`. +6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same name and email. The installer asks once whether the gitflow hooks push every commit and merge (`true` or `false` exactly, Enter = `true`); a `true`/`false` already in `~/.gitconfig` is reused without asking and survives the redeploy; `DOTFILES_GITFLOW_AUTOPUSH=true|false` presets it for a non-interactive install (no terminal and no preset gives `true`; any other preset aborts the install); the render refuses to write a file where `@AUTOPUSH@` leaked, since a non-boolean value blocks every push. To switch later: `git config --global gitflow.autopush true|false`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`. 7. Installs Python CLIs via `pipx` (`PyMuPDF` → `pymupdf`, `Markdown` → `markdown_py`) — skipped if `pipx` is absent. 8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. The dtach session-resume menu ships in the deployed bashrc (both OSes), so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read `~/.profile`. The installer also strips any older dtach block left in `~/.profile` so a plain SSH login doesn't prompt twice. 9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage. diff --git a/gitconfig b/gitconfig index e2ec94b..684bf06 100644 --- a/gitconfig +++ b/gitconfig @@ -1,10 +1,14 @@ -# Template for the user-scope ~/.gitconfig, rendered by install.sh. -# Git never expands $VARS: @USER@ and @EMAIL@ are replaced at install -# time with the USER and EMAIL exported by the deployed bashrc. -# A repo .git/config still overrides these values for that repo. +# Template for the user-scope ~/.gitconfig, rendered by install.sh. Git never +# expands $VARS, so the identity and the gitflow push mode are placeholders +# filled at install time with the installer's answers. A repo .git/config +# still overrides these values for that repo. [user] name = @USER@ email = @EMAIL@ +[gitflow] + # Push mode of the gitflow hooks: false = manual, you run `git push`; + # true = every commit and merge is pushed. Exact true/false only (fail-closed). + autopush = @AUTOPUSH@ [push] default = current [color] @@ -15,6 +19,8 @@ editor = vim pager = delta excludesfile = ~/.gitignore + # gitflow hooks (created by `make link` in claude-config); git expands ~ itself. + hooksPath = ~/.claude/githooks [advice] detachedHead = false [merge] diff --git a/install.sh b/install.sh index 2f7a357..4b2b13c 100755 --- a/install.sh +++ b/install.sh @@ -2,6 +2,9 @@ # install.sh — deploy the vim + bash dotfiles. OS is auto-detected. # Usage: ./install.sh set -euo pipefail +# bash >= 5.2 expands `&` in ${var//pat/rep} replacements: an `&` in a name would +# corrupt the rendered identity. No-op on bash 3.2. +shopt -u patsub_replacement 2>/dev/null || true # Resolve the repo root so the script works from any working directory. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -524,20 +527,75 @@ resolve_identity() { printf '%s\n' "${value:-$default}" } +# Ask the push question on the terminal until the answer is exactly true or false; +# Enter (or EOF) = true. Prints the value. +ask_autopush() { + local answer="" + while :; do + read -rp "Automatic push of commits by the gitflow hooks on this machine? [true/false] (default: true) " answer || true + case "${answer:-true}" in + true|false) printf '%s\n' "${answer:-true}"; return 0 ;; + *) echo "Answer exactly true or false." >&2 ;; + esac + done +} + +# Push mode of the gitflow hooks (gitflow.autopush), exact true/false only: the +# readers fail closed on anything else. Never asked twice: a true/false already in +# ~/.gitconfig wins silently (read with sed, like rc_export_value, so a hand-set +# value survives the redeploy; a non-exact spelling is named and re-asked), else +# DOTFILES_GITFLOW_AUTOPUSH (anything but true/false aborts the install here, +# before any file is touched, whatever ~/.gitconfig holds), else the prompt +# on a terminal, else true (today's unset = auto). Prints the value. +resolve_autopush() { + local value="" preset="${DOTFILES_GITFLOW_AUTOPUSH:-}" + case "$preset" in + true|false|"") ;; + *) echo "DOTFILES_GITFLOW_AUTOPUSH='$preset' — must be exactly true or false" >&2; return 1 ;; + esac + if [ -f "$HOME/.gitconfig" ]; then + value="$(sed -n 's/^[[:space:]]*autopush[[:space:]]*=[[:space:]]*//p' "$HOME/.gitconfig" | tail -n 1)" + case "$value" in + true|false) printf '%s\n' "$value"; return 0 ;; + "") ;; + *) echo "gitflow.autopush='$value' in ~/.gitconfig is not exactly true/false — asking again" >&2 ;; + esac + fi + if [ -n "$preset" ]; then printf '%s\n' "$preset"; return 0; fi + if [ -t 0 ]; then ask_autopush; else echo true; fi +} + +# Print the gitconfig template with the identity ($1 name, $2 email) and the push +# mode ($3) filled in. Fails, printing nothing, when the mode is not exactly +# true/false or when @AUTOPUSH@ survives the render: a non-boolean +# gitflow.autopush blocks every push, so a leaked placeholder is an outage. +render_gitconfig() { + local name="$1" email="$2" autopush="$3" rendered + case "$autopush" in + true|false) ;; + *) echo "gitconfig render refused: push mode '$autopush' is not exactly true/false — nothing written" >&2; return 1 ;; + esac + rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")" + rendered="${rendered//@AUTOPUSH@/$autopush}" + case "$rendered" in + *@AUTOPUSH@*) echo "gitconfig render failed: @AUTOPUSH@ left in the output — nothing written" >&2; return 1 ;; + esac + printf '%s\n' "$rendered" +} + # Install the user-scope ~/.gitconfig (a repo's own .git/config still wins). -# The identity is read from the USER/EMAIL exports of the deployed rc ($1), so -# git and the shell agree. A ~/.gitconfig that differs is kept as +# $1 $2 = the identity rendered into the rc, so git and the shell agree; $3 = the +# gitflow push mode (true/false). A ~/.gitconfig that differs is kept as # ~/.gitconfig.backup-, outside ~/Oldconfig which every run wipes. -# Idempotent: an identical ~/.gitconfig is left alone. +# Idempotent: an identical ~/.gitconfig is left alone. Two fail-closed checks +# live in render_gitconfig: exact push mode, no leaked placeholder. deploy_gitconfig() { - local rc="$1" name email rendered backup - name="$(rc_export_value USER "$rc")" - email="$(rc_export_value EMAIL "$rc")" + local name="$1" email="$2" autopush="$3" rendered backup if [ -z "$name" ] || [ -z "$email" ]; then - echo "USER/EMAIL not exported by $rc — skipping ~/.gitconfig" >&2 + echo "Name or email empty — skipping ~/.gitconfig (push mode $autopush not written)" >&2 return 0 fi - rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")" + rendered="$(render_gitconfig "$name" "$email" "$autopush")" || return 1 if printf '%s\n' "$rendered" | cmp -s - "$HOME/.gitconfig"; then echo "$HOME/.gitconfig already up to date — skipping" return 0 @@ -547,7 +605,7 @@ deploy_gitconfig() { echo "Saving the current ~/.gitconfig to $backup" mv "$HOME/.gitconfig" "$backup" fi - echo "Deploying gitconfig to ~/.gitconfig ($name <$email>)" + echo "Deploying gitconfig to ~/.gitconfig ($name <$email>, autopush=$autopush)" printf '%s\n' "$rendered" > "$HOME/.gitconfig" } @@ -584,6 +642,7 @@ EOF identity_name="$(resolve_identity USER "Name for git commits and vim headers" "$(id -un)")" identity_email="$(resolve_identity EMAIL "Email for git commits and vim headers" "")" echo "Identity: $identity_name <${identity_email:-no email}>" +autopush="$(resolve_autopush)" # System packages: apt-get on Debian/Ubuntu, Homebrew on macOS. if command -v apt-get >/dev/null 2>&1; then @@ -684,8 +743,8 @@ fi echo "Deploying $bashrc ($identity_name <$identity_email>)" render_identity_template "$SCRIPT_DIR/$bashrc" "$identity_name" "$identity_email" > "$HOME/.bashrc" -# User-scope git config, identity taken from the bashrc just deployed. -deploy_gitconfig "$SCRIPT_DIR/$bashrc" +# User-scope git config, same identity as the rc just rendered. +deploy_gitconfig "$identity_name" "$identity_email" "$autopush" # tmux config + plugins (tmux comes from the apt or brew list above). if command -v tmux >/dev/null 2>&1; then From 252954b803c78e3ec3b05f277eb167a18b38ae32 Mon Sep 17 00:00:00 2001 From: bchanot Date: Wed, 7 Oct 2026 18:23:25 +0200 Subject: [PATCH 2/4] =?UTF-8?q?docs(claude):=20layout=20line=20=E2=80=94?= =?UTF-8?q?=20gitconfig=20template=20carries=20@AUTOPUSH@=20and=20a=20fixe?= =?UTF-8?q?d=20core.hooksPath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 572d2a0..251e490 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,8 @@ vim/autoload/ pathogen loader (committed) vim/colors/ molokai colorscheme (committed) bash/bashrc-{linux,osx} OS-detected bashrc; USER/EMAIL identity = @USER@/@EMAIL@ placeholders, asked at install (reused from an existing rc), never tracked -gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@ filled at install +gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@/@AUTOPUSH@ + (gitflow push mode, exact true/false) filled at install; core.hooksPath fixed tmux.conf tmux config (vi keys, tpm plugins) → ~/.config/tmux/tmux.conf, both OSes (tmux ≥ 3.1) zsh/{zshrc-osx,bchanot.zsh-theme} macOS zsh option: oh-my-zsh zshrc + theme porting the bash prompt bin/{dt,dtach-router,claude-provider} CLI scripts deployed to ~/.local/bin From 30b330967f3c741f38a61a3ff0b3cb1e053b3013 Mon Sep 17 00:00:00 2001 From: bchanot Date: Wed, 7 Oct 2026 18:25:55 +0200 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20README=20step=206=20precedence,=20e?= =?UTF-8?q?mpty-identity=20skip,=20fixed=20hooksPath=20+=20global=20caveat?= =?UTF-8?q?,=20macOS=20question=20order;=20CHANGELOG=20Unreleased=20?= =?UTF-8?q?=E2=80=94=20feat=20gitconfig-autopush?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ README.md | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 791f9f9..3169859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: se ## [Unreleased] +### Added +- `install.sh` asks the gitflow push mode (`gitflow.autopush`, exactly `true` or `false`, Enter = `true`) and writes it to `~/.gitconfig`; an existing value is reused, `DOTFILES_GITFLOW_AUTOPUSH` presets it, any other preset aborts the install. +- `gitconfig`: fixed `core.hooksPath = ~/.claude/githooks`. + +### Fixed +- `~/.gitconfig` was deployed with the literal `@USER@` / `@EMAIL@` placeholders: the installer read the identity from the repo bashrc template instead of the answers. +- An `&` in the name no longer corrupts the rendered identity on bash 5.2 and later (`patsub_replacement` turned off). + ## [1.0.0] — 2026-10-06 ### Added diff --git a/README.md b/README.md index 0f4342f..a4b534e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/main/remote-install. | `vim/vimrc` | Vim config: pathogen, molokai, syntastic (C with `-Wall -Werror -Wextra`), NERDTree, 42-style canonical class generators (`:ClassH`, `:ClassC`). | | `vim/autoload/` | `pathogen.vim` plugin loader (committed). | | `vim/colors/` | `molokai.vim` colorscheme (committed). | -| `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@`, `@EMAIL@` and `@AUTOPUSH@` (gitflow push mode) are filled at install with the installer's answers (git never expands `$VARS` itself); `core.hooksPath` points at the gitflow hooks `make link` creates. | +| `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@`, `@EMAIL@` and `@AUTOPUSH@` (gitflow push mode) are filled at install with the installer's answers (git never expands `$VARS` itself); `core.hooksPath` points at the gitflow hooks `make link` creates in the claude-config repo. | | `bash/bashrc-linux` | bashrc for desktop Linux (git-aware prompt + command timer). | | `bash/bashrc-osx` | bashrc for macOS: `bashrc-linux` adapted (Homebrew on `PATH`, BSD `ls -G`, bash 5 clock for the timer, `cc` without `systemd-run`). | | `zsh/zshrc-osx` | zshrc for macOS when zsh is chosen: oh-my-zsh + the same env, aliases and dtach menu as `bashrc-osx`. Loads `~/.zshrc.local` for machine-specific lines. | @@ -67,7 +67,7 @@ What it does: 3. Moves any existing `~/.vim`, `~/.vimrc`, `~/.bashrc`, `~/.Sublivim` to `~/Oldconfig`. 4. Clones the `syntastic` and `nerdtree` vim plugins into `~/.vim/bundle/`. 5. Copies the tracked vim files into `~/.vim` and symlinks `~/.vimrc`. -6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same name and email. The installer asks once whether the gitflow hooks push every commit and merge (`true` or `false` exactly, Enter = `true`); a `true`/`false` already in `~/.gitconfig` is reused without asking and survives the redeploy; `DOTFILES_GITFLOW_AUTOPUSH=true|false` presets it for a non-interactive install (no terminal and no preset gives `true`; any other preset aborts the install); the render refuses to write a file where `@AUTOPUSH@` leaked, since a non-boolean value blocks every push. To switch later: `git config --global gitflow.autopush true|false`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`. +6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same name and email (skipped, push mode included, when the name or email is empty, e.g. a first install with no terminal and no `IDENTITY_EMAIL`). The installer asks once whether the gitflow hooks push every commit and merge (`true` or `false` exactly, Enter = `true`); a `true`/`false` already in `~/.gitconfig` is reused without asking and survives the redeploy; `DOTFILES_GITFLOW_AUTOPUSH=true|false` presets it for a non-interactive install, an existing `~/.gitconfig` value still winning (no terminal and no preset gives `true`; any other preset aborts the install); the render refuses to write a file where `@AUTOPUSH@` leaked, since a non-boolean value blocks every push. To switch later: `git config --global gitflow.autopush true|false`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. `core.hooksPath` is fixed to `~/.claude/githooks`, the gitflow hooks created by `make link` in the claude-config repo; the installer neither creates nor checks that directory. Being global, it makes git ignore each repo's `.git/hooks/` unless that repo sets its own `core.hooksPath`. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`. 7. Installs Python CLIs via `pipx` (`PyMuPDF` → `pymupdf`, `Markdown` → `markdown_py`) — skipped if `pipx` is absent. 8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. The dtach session-resume menu ships in the deployed bashrc (both OSes), so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read `~/.profile`. The installer also strips any older dtach block left in `~/.profile` so a plain SSH login doesn't prompt twice. 9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage. @@ -99,7 +99,7 @@ The script is re-runnable: each run re-backs up to `~/Oldconfig` (overwriting th ### macOS -The same `./install.sh` detects macOS and replaces `apt-get` with Homebrew. It first asks which login shell you want, **zsh** or **bash** (`[zsh]` by default; answer in advance with `MACOS_SHELL=bash ./install.sh`, and with no terminal attached it picks zsh): +The same `./install.sh` detects macOS and replaces `apt-get` with Homebrew. After the identity and push-mode questions, it asks which login shell you want, **zsh** or **bash** (`[zsh]` by default; answer in advance with `MACOS_SHELL=bash ./install.sh`, and with no terminal attached it picks zsh): 1. Installs Homebrew with its official script when `brew` is missing (this also pulls the Xcode Command Line Tools: clang, make, git), then `brew update` + `brew upgrade`. 2. Installs the apt list mapped to formulae: `vim git git-lfs git-filter-repo gitleaks pkgconf shellcheck gh git-delta curl gnupg lftp inetutils unzip tree tmux fzf dtach node python pipx php mariadb imagemagick ffmpeg weasyprint poppler qpdf webp libavif bash`. Brew's `php` already ships gd, mbstring, xml, intl, curl and mysql. From c2196424b9452f2e82856221afee45d67d896a84 Mon Sep 17 00:00:00 2001 From: bchanot Date: Wed, 7 Oct 2026 18:25:56 +0200 Subject: [PATCH 4/4] =?UTF-8?q?chore(memory):=20journal=20+=20contract/pla?= =?UTF-8?q?n/oracle=20=E2=80=94=20feat=20gitconfig-autopush?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/journal.md | 3 + .claude/tasks/TODO.md | 4 + .../2026-10-07-gitconfig-autopush-1734.md | 48 ++++ .../tasks/contracts/check-autopush-render.sh | 94 +++++++ .../2026-10-07-gitconfig-autopush-1734.md | 247 ++++++++++++++++++ 5 files changed, 396 insertions(+) create mode 100644 .claude/tasks/contracts/2026-10-07-gitconfig-autopush-1734.md create mode 100644 .claude/tasks/contracts/check-autopush-render.sh create mode 100644 .claude/tasks/plans/2026-10-07-gitconfig-autopush-1734.md diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 4f8f596..4518866 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -120,3 +120,6 @@ Done: `bin/repo-sync` (gitlab/github/gitea/bitbucket cloud via curl+jq, daily ca ## 2026-10-07 (2) — macOS zsh default, remote-install prompts Done: choose_macos_shell default bash → zsh (Enter, no tty, unknown value). Found: `curl | bash` path never asked USER/EMAIL (stdin = pipe → `[ -t 0 ]` false → login name + empty email, bash, no offers); remote-install.sh now runs install.sh ``; no other template section touched. +- No test suite / no --dry-run exists: the oracle harness carries the two requested cases (rendered @AUTOPUSH@, leaked placeholder = failure). +- The user sees the full diff before anything is committed. + +## ACCEPTANCE CRITERIA +1. `gitconfig` template: `[gitflow]` section with `autopush = @AUTOPUSH@`, and `hooksPath = ~/.claude/githooks` inside `[core]`; other sections unchanged except the header comment. + CHECK: grep -q '^\[gitflow\]' gitconfig && grep -q '^ autopush = @AUTOPUSH@' gitconfig && grep -q '^ hooksPath = ~/.claude/githooks' gitconfig && echo GITCONFIG_TEMPLATE_OK + EXPECT: GITCONFIG_TEMPLATE_OK + EVIDENCE: MET exit=0 marker-found :: GITCONFIG_TEMPLATE_OK +2. `install.sh` resolves the push mode: an exact `true`/`false` already in ~/.gitconfig wins silently; else `DOTFILES_GITFLOW_AUTOPUSH` (exact true/false; any other value aborts with a message); else a tty prompt accepting only `true`/`false`, re-asking otherwise, Enter = `true`; else `true`. +3. Oracle harness: rendered file carries the resolved autopush, name, email, hooksPath and no placeholder; a leaked `@AUTOPUSH@` makes deploy_gitconfig fail without writing; existing value beats the preset; strict preset; prompt loop; empty email skip; idempotent re-run. + CHECK: bash .claude/tasks/contracts/check-autopush-render.sh + EXPECT: AUTOPUSH_RENDER_OK + EVIDENCE: MET exit=0 marker-found :: AUTOPUSH_RENDER_OK +4. install.sh stays syntactically valid and shellcheck clean. + CHECK: bash -n install.sh && shellcheck install.sh && echo LINT_OK + EXPECT: LINT_OK + EVIDENCE: MET exit=0 marker-found :: LINT_OK +5. README.md (gitconfig row, remote-install question list, install step 6) and CLAUDE.md layout line document the question, the strict values, the env preset, the reuse of an existing value and the fixed hooksPath. +6. Call site passes the resolved identity, never the repo template path. + CHECK: grep -q 'deploy_gitconfig "\$identity_name" "\$identity_email" "\$autopush"' install.sh && ! grep -q 'deploy_gitconfig "\$SCRIPT_DIR' install.sh && echo CALLSITE_OK + EXPECT: CALLSITE_OK + EVIDENCE: MET exit=0 marker-found :: CALLSITE_OK +7. `.githooks/post-commit` and `.githooks/post-merge` are not part of the change (pre-existing session-hook refresh). + CHECK: git diff --cached --name-only | grep -q '^\.githooks/' && exit 1; echo HOOKS_UNTOUCHED + EXPECT: HOOKS_UNTOUCHED + EVIDENCE: MET exit=0 marker-found :: HOOKS_UNTOUCHED + +## FILE SCOPE +gitconfig, install.sh, README.md, CLAUDE.md, .claude/tasks/contracts/check-autopush-render.sh (oracle harness, LRN-011 pattern) diff --git a/.claude/tasks/contracts/check-autopush-render.sh b/.claude/tasks/contracts/check-autopush-render.sh new file mode 100644 index 0000000..8fde377 --- /dev/null +++ b/.claude/tasks/contracts/check-autopush-render.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Oracle for the contract: exercises install.sh functions on scratch HOMEs under +# mktemp; never reads or writes the real ~/.gitconfig and never calls git config. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +SCRIPT_DIR="$PWD" + +fail() { echo "FAIL: $*" >&2; exit 1; } +fresh_home() { HOME="$(mktemp -d)"; export HOME; } +guard_home() { + case "$HOME" in /tmp/*|/private/*|/var/*) ;; *) fail "refusing HOME=$HOME" ;; esac +} +has() { grep -qF -- "$2" "$1" || fail "$1 lacks: $2"; } + +funcs="$(mktemp)" +for fn in ask_autopush resolve_autopush render_identity_template \ + render_gitconfig deploy_gitconfig; do + sed -n "/^$fn() {/,/^}/p" install.sh >> "$funcs" +done +source "$funcs" +shopt -u patsub_replacement 2>/dev/null || true +unset DOTFILES_GITFLOW_AUTOPUSH + +# Case A: render +fresh_home; guard_home +[ "$(DOTFILES_GITFLOW_AUTOPUSH=false resolve_autopush /dev/null +for l in 'autopush = false' 'name = x' 'email = x@y' 'hooksPath = ~/.claude/githooks'; do + has "$HOME/.gitconfig" "$l" +done +! grep -qE '@(USER|EMAIL|AUTOPUSH)@' "$HOME/.gitconfig" || fail "A: placeholder left" + +# Case I: idempotent re-run in A's HOME +out="$(deploy_gitconfig x x@y false)" +case "$out" in *"already up to date"*) ;; *) fail "I: not skipped: $out" ;; esac +ls "$HOME"/.gitconfig.backup-* >/dev/null 2>&1 && fail "I: backup created" + +# Case A2: ampersand +fresh_home; guard_home +deploy_gitconfig 'a & b' x@y true >/dev/null +has "$HOME/.gitconfig" 'name = a & b' + +# Case B: leaked placeholder +fresh_home; guard_home +set +e; deploy_gitconfig x x@y '@AUTOPUSH@' 2>"$HOME/err"; rc=$?; set -e +[ "$rc" -ne 0 ] || fail "B: rc 0" +has "$HOME/err" '@AUTOPUSH@' +[ ! -e "$HOME/.gitconfig" ] || fail "B: file written" + +# Case C: existing wins over preset +fresh_home; guard_home +printf '[gitflow]\n autopush = true\n' > "$HOME/.gitconfig" +[ "$(DOTFILES_GITFLOW_AUTOPUSH=false resolve_autopush "$HOME/err" "$HOME/err")" = true ] || fail "F1" +has "$HOME/err" exactly +[ "$(printf '\n' | ask_autopush)" = true ] || fail "F2" + +# Case G: empty email +fresh_home; guard_home +deploy_gitconfig x "" true 2>"$HOME/err" +[ ! -e "$HOME/.gitconfig" ] || fail "G: file written" +has "$HOME/err" "not written" + +# Case H: call site +grep -q 'deploy_gitconfig "\$identity_name" "\$identity_email" "\$autopush"' install.sh \ + || fail "H: call site" +! grep -q 'deploy_gitconfig "\$SCRIPT_DIR' install.sh || fail "H: template path" + +# Case J: non-exact existing value +fresh_home; guard_home +printf '[gitflow]\n autopush = off\n' > "$HOME/.gitconfig" +[ "$(resolve_autopush 2>"$HOME/err" "$HOME/err"; rc=$?; set -e +[ "$rc" -ne 0 ] || fail "K: rc 0" +has "$HOME/err" "not exactly" +[ ! -e "$HOME/.gitconfig" ] || fail "K: file written" + +echo AUTOPUSH_RENDER_OK diff --git a/.claude/tasks/plans/2026-10-07-gitconfig-autopush-1734.md b/.claude/tasks/plans/2026-10-07-gitconfig-autopush-1734.md new file mode 100644 index 0000000..4604fa6 --- /dev/null +++ b/.claude/tasks/plans/2026-10-07-gitconfig-autopush-1734.md @@ -0,0 +1,247 @@ +# PLAN — gitconfig-autopush (feat) — REVISED v4 (round 1 + user spec + confirmation pass, default true) +Contract: .claude/tasks/contracts/2026-10-07-gitconfig-autopush-1734.md +Branch: feature/gitconfig-autopush (off develop) + +## Goal (user spec, verbatim constraints in the contract) +1. `gitconfig` template, section `[core]`: fixed line `hooksPath = ~/.claude/githooks` + (git expands `~` itself for core.hooksPath; no absolute path, no expansion by the + script; the installer neither creates nor checks that directory). +2. install.sh asks at install: automatic push by the gitflow hooks on this machine? + `[true/false]` (default: true — user correction). Answer rendered into a new section + `[gitflow]` / `autopush = @AUTOPUSH@`. + - Only the exact strings `true` and `false` are accepted. Anything else re-asks; + an empty answer gives `true`. + - The render FAILS explicitly (grep after render) if `@AUTOPUSH@` is still in the + final content. A non-boolean value blocks every push on the machine (fail-closed), + so a leaked placeholder would be a silent outage. + - Non-interactive install: env `DOTFILES_GITFLOW_AUTOPUSH=true|false`, default true. +3. Keep the existing backup (`~/.gitconfig.backup-`). Do not touch the other + template sections. +4. Human-gated 2026-10-07 (kept from round 1): fix the identity call site so the deployed + file carries the resolved name/email, not the literal `@USER@`/`@EMAIL@`. +5. The user sees the full diff BEFORE anything is committed (working tree only until then). + +## Context (verified) +- `gitconfig` is a template; `render_identity_template` (install.sh:497) replaces + @USER@/@EMAIL@ by bash substitution over every line (comments included). +- `deploy_gitconfig` (install.sh:532) takes an rc PATH and reads USER/EMAIL from it. + BUG: the call site install.sh:688 passes `"$SCRIPT_DIR/$bashrc"` = the repo TEMPLATE + whose exports are `export USER="@USER@"` (bash/bashrc-linux:33-34). The placeholders + are non-empty, the guard passes, the deployed file reads `name = @USER@`. +- Identity resolved at install.sh:584-585 into `identity_name` / `identity_email`, before + package installs (prompts first). EMAIL default "" when no tty and no env. +- Readers (~/.claude/lib/gitflow.sh gitflow_push_mode, post-commit/post-merge hooks): + `git config --bool gitflow.autopush` → false = manual, true/unset = auto, other = + invalid → nothing pushed (fail closed). Only exact `true`/`false` are ever written. +- `core.hooksPath` is a pathname-typed key: git tilde-expands `~/.claude/githooks`. + Today `make link` writes it with `git config --global`; the redeploy dropped it. With + the fixed template line the redeploy keeps it and the file matches the render again. +- This session's permission layer denies every `git config … gitflow.*` command, so the + installer and the oracle must NOT read or write the key through git: an existing value + is read from ~/.gitconfig with sed (same shape as rc_export_value). Trade-off: a value + set in XDG/system/included config is not seen; ~/.gitconfig is the only file written. +- install.sh runs under macOS /bin/bash 3.2 before brew bash exists (LRN-014): plain + string compares only, no `${var,,}`. +- The repo has no test suite and install.sh has no --dry-run: the oracle harness below is + the test, run by the contract gate. + +## Checklist + [ ] gitconfig — header comment (lines 1-4) reworded WITHOUT literal placeholder tokens + (today it renders as "bchanot and x@y are replaced at install time"): "Template for + the user-scope ~/.gitconfig, rendered by install.sh. Git never expands $VARS, so the + identity and the gitflow push mode are placeholders filled at install time with the + installer's answers. A repo .git/config still overrides these values for that repo." + In `[core]`, after `excludesfile`, add the fixed line: + hooksPath = ~/.claude/githooks + with a one-line comment above it: "# gitflow hooks (created by `make link` in + claude-config); git expands ~ itself." After `[user]`, add: + [gitflow] + # Push mode of the gitflow hooks: false = manual, you run `git push`; + # true = every commit and merge is pushed. Exact true/false only (fail-closed). + autopush = @AUTOPUSH@ + 4-space indent like the other keys. No other section touched. + [ ] install.sh — two new functions right after `resolve_identity` (~line 525), tabs: + # Ask the push question on the terminal until the answer is exactly true or false; + # Enter (or EOF) = true. Prints the value. + ask_autopush() { + local answer="" + while :; do + read -rp "Automatic push of commits by the gitflow hooks on this machine? [true/false] (default: true) " answer || true + case "${answer:-true}" in + true|false) printf '%s\n' "${answer:-true}"; return 0 ;; + *) echo "Answer exactly true or false." >&2 ;; + esac + done + } + + # Push mode of the gitflow hooks (gitflow.autopush), exact true/false only: the + # readers fail closed on anything else. Never asked twice: a true/false already in + # ~/.gitconfig wins silently (read with sed, like rc_export_value, so a hand-set + # value survives the redeploy; a non-exact spelling is named and re-asked), else + # DOTFILES_GITFLOW_AUTOPUSH (anything but true/false aborts the install here, + # before any file is touched, whatever ~/.gitconfig holds), else the prompt + # on a terminal, else true (today's unset = auto). Prints the value. + resolve_autopush() { + local value="" preset="${DOTFILES_GITFLOW_AUTOPUSH:-}" + case "$preset" in + true|false|"") ;; + *) echo "DOTFILES_GITFLOW_AUTOPUSH='$preset' — must be exactly true or false" >&2; return 1 ;; + esac + if [ -f "$HOME/.gitconfig" ]; then + value="$(sed -n 's/^[[:space:]]*autopush[[:space:]]*=[[:space:]]*//p' "$HOME/.gitconfig" | tail -n 1)" + case "$value" in + true|false) printf '%s\n' "$value"; return 0 ;; + "") ;; + *) echo "gitflow.autopush='$value' in ~/.gitconfig is not exactly true/false — asking again" >&2 ;; + esac + fi + if [ -n "$preset" ]; then printf '%s\n' "$preset"; return 0; fi + if [ -t 0 ]; then ask_autopush; else echo true; fi + } + Order: preset syntax is validated FIRST (a bad preset always aborts, even on a + re-run), then an existing exact value wins, then a valid preset, prompt, default. + Each ≤ 25 logic lines. `read … || true` on EOF → empty → true → loop ends. + [ ] install.sh — after `set -euo pipefail` (line 4) add, with a one-line comment + ("bash ≥ 5.2 expands `&` in ${var//pat/rep} replacements: an `&` in a name would + corrupt the rendered identity; no-op on bash 3.2"): + shopt -u patsub_replacement 2>/dev/null || true + [ ] install.sh — main sequence, right after identity_email (line 585): + autopush="$(resolve_autopush)" + (set -e: a `return 1` from an invalid preset aborts the install right here.) + [ ] install.sh — `deploy_gitconfig` becomes `deploy_gitconfig ` + (no rc path, no rc_export_value call): + local name="$1" email="$2" autopush="$3" rendered backup + if [ -z "$name" ] || [ -z "$email" ]; then + echo "Name or email empty — skipping ~/.gitconfig (push mode $autopush not written)" >&2 + return 0 + fi + rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")" + case "$autopush" in true|false) ;; *) + echo "gitconfig render refused: push mode '$autopush' is not exactly true/false — nothing written" >&2 + return 1 ;; + esac + rendered="${rendered//@AUTOPUSH@/$autopush}" + case "$rendered" in *@AUTOPUSH@*) + echo "gitconfig render failed: @AUTOPUSH@ left in the output — nothing written" >&2 + return 1 ;; + esac + (no pipe: `printf | grep -q` under pipefail can read SIGPIPE as "no leak") + … cmp/skip, backup, write unchanged … + echo "Deploying gitconfig to ~/.gitconfig ($name <$email>, autopush=$autopush)" + Header comment rewritten: $1 $2 = the identity rendered into the rc, $3 = push mode; + one line on the two fail-closed checks. If > 25 logic lines, extract + `render_gitconfig ` (render + substitution + grep). + [ ] install.sh:688 call site → `deploy_gitconfig "$identity_name" "$identity_email" "$autopush"` + comment: "User-scope git config, same identity as the rc just rendered." + [ ] README.md:31 gitconfig row — "`@USER@`, `@EMAIL@` and `@AUTOPUSH@` (gitflow push + mode) are filled at install with the installer's answers; `core.hooksPath` points at + the gitflow hooks `make link` creates." + [ ] README.md:50 — "(identity, push mode, macOS shell, offers)". + [ ] README.md:70 step 6 — replace "with the same `USER` / `EMAIL`" by "with the same name + and email", then add: the installer asks once whether the gitflow hooks push every + commit and merge (`true`/`false` exactly, Enter = `true`); a `true`/`false` already + in `~/.gitconfig` is reused without asking and survives the redeploy; + `DOTFILES_GITFLOW_AUTOPUSH=true|false` presets it for a non-interactive install (no + terminal and no preset → `true`; any other preset aborts the install); the render + refuses to write a file where `@AUTOPUSH@` leaked, since a non-boolean value blocks + every push. To switch later: `git config --global gitflow.autopush true|false`. + [ ] CLAUDE.md:25 — `gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@/@AUTOPUSH@ + (gitflow push mode, exact true/false) filled at install; core.hooksPath fixed` + [ ] .claude/tasks/contracts/check-autopush-render.sh — oracle harness (LRN-011 pattern): + `set -euo pipefail`; `cd "$(git rev-parse --show-toplevel)"` (the only git call, no + config access); `SCRIPT_DIR="$PWD"`. Extract ask_autopush, resolve_autopush, + render_identity_template, deploy_gitconfig (and render_gitconfig if extracted) from + install.sh via `sed -n '/^fn() {/,/^}/p'` into `$(mktemp)` and source it. + `fresh_home() { HOME="$(mktemp -d)"; export HOME; }` before every case; guard + `case "$HOME" in /tmp/*|/private/*|/var/*) ;; *) fail "refusing HOME=$HOME";; esac` + before any deploy call. No rm of temp dirs (never rm -rf through a variable). + `fail() { echo "FAIL: $*" >&2; exit 1; }`; every assertion goes through it. + stderr captures go to `"$HOME/err"` (the scratch HOME), never a relative path in + the repo. Header comment: "Oracle for the contract: exercises install.sh functions + on scratch HOMEs under mktemp; never reads or writes the real ~/.gitconfig and never + calls git config." If the permission layer refuses a step of this harness, the + refusal is reported with its rule and the harness is handed to the user to run; it + is never reworked to get around the rule. + Case A (render): fresh_home; DOTFILES_GITFLOW_AUTOPUSH=false err; rc=$?; set -e` → rc != 0, err contains "@AUTOPUSH@", no + ~/.gitconfig written. (Passing the placeholder as the value is the only way to + make the substitution a no-op; it stands in for a broken template.) + Case C (existing wins): fresh_home; printf '[gitflow]\n autopush = true\n' > + "$HOME/.gitconfig"; DOTFILES_GITFLOW_AUTOPUSH=false err; rc=$?; set -e` → rc != 0, err contains "exactly". + Case F (prompt loop): `printf 'yes\ntrue\n' | ask_autopush 2>err` → true, err + contains "exactly"; `printf '\n' | ask_autopush` → true. + Case G (empty email): deploy_gitconfig x "" true 2>err → no file, err has "not written". + Case H (call site): `grep -q 'deploy_gitconfig "\$identity_name" "\$identity_email" "\$autopush"' install.sh` + and `! grep -q 'deploy_gitconfig "\$SCRIPT_DIR' install.sh`. + Case I (idempotent): run right after Case A in Case A's HOME (no fresh_home between + them): deploy_gitconfig x x@y false again → stdout contains "already up to date", + no `.gitconfig.backup-*` created. + Case J (non-exact existing): fresh_home; printf '[gitflow]\n autopush = off\n' > + "$HOME/.gitconfig"; env unset, "$HOME/err"; rc=$?; set -e` + → rc != 0, err contains "not exactly", no file. + Print AUTOPUSH_RENDER_OK only at the end. + +## Edge cases +- Non-exact existing value: `off`/`no`/`0` are VALID false for the readers (`--bool`), + `maybe` is invalid (nothing pushed). Neither is reused (strict grammar): the value is + named on stderr, then preset/prompt/default decide. With no tty and no preset a + hand-set `off` becomes `true`: accepted consequence of the strict grammar + default + true, documented in the README sentence on switching the mode. +- Re-run: value found in ~/.gitconfig → no prompt → identical render → "already up to + date — skipping" (now holds with hooksPath in the template). +- No tty (curl | bash without /dev/tty) and no preset → true: same as today's unset = auto. +- Invalid preset aborts before Oldconfig/rm or any write (resolution happens at the + identity step). +- Empty email → file skipped, dropped push mode named in the warning. +- `.githooks/post-commit` / `.githooks/post-merge` are dirty from the session hook + refresh: do not touch, do not stage. +- Known, not handled: a system-scope `/etc/gitconfig` value is shadowed by the written + global one (blockers entry at CAPITALIZE). + +## Tests +- `bash -n install.sh`, `shellcheck install.sh`. +- `bash .claude/tasks/contracts/check-autopush-render.sh` → AUTOPUSH_RENDER_OK. + +## Disposition (memory read-before) +- honors BDR-001 (bash) — bash substitutions, `read -rp`. +- honors BDR-002 — template read via `$SCRIPT_DIR/gitconfig`. +- BDR-012 pattern (installer prompts, re-ask on bad input, keep-existing, no TTY → + default) honored on the interactive path; DEVIATION flagged: an invalid + DOTFILES_GITFLOW_AUTOPUSH preset aborts the install (user's fail-closed requirement), + before any file is touched. +- honors LRN-001 idempotency — unchanged ~/.gitconfig skipped; existing value reused. +- honors LRN-011 — oracle is a stub harness on extracted functions, no live install, no git config. +- honors LRN-014 — bash 3.2 safe. +- Non-binding: BDR-016 (backup naming, kept as is). + +## CHALLENGE SUMMARY (round 1 → v2) +- BLOCKER (correctness+robustness): template path at the call site → placeholders deployed + → closed: deploy_gitconfig takes name/email/autopush; call site passes the resolved + identity; Case A asserts no placeholder; Case H pins the call site. [gated] +- BLOCKER (simplicity+robustness): `git config … gitflow.*` denied to this session → + closed: sed read of ~/.gitconfig, harness writes files directly, zero config access. +- MAJOR: loose boolean grammar / fail-open normaliser → superseded by the user spec: + exact true/false only, re-ask, default true (user correction), invalid preset aborts. +- MAJOR: empty email drops the answer → closed: named in the skip warning. +- MAJOR: hooksPath wiped on redeploy → superseded by the user spec: fixed template line. +- MAJOR: shared scratch HOME → closed: fresh_home per case + guard. +- MINORs: header comment reworded, `@(USER|EMAIL|AUTOPUSH)@` assertion, README:50, README + wording (precedence + how to switch), call-site grep. Deferred: system-scope shadowing. +- Added by the user spec: fail-closed grep after render (Case B), DOTFILES_ prefix. + +## CHALLENGE SUMMARY (confirmation pass, v3 → v4): SOLID, 8 MINOR +- Accepted: preset validated first; non-exact existing value named; positive true/false + guard + no-pipe leak check; patsub_replacement off + Case A2; err files in scratch HOME; + Case I in Case A's HOME; harness header + refusal rule; TODO line rewritten. +- Edge-case text corrected (off/no/0 are valid false for the readers). +- BDR-012 deviation (preset abort) flagged in the Disposition.