Git for Windows nagging "Unlink failed. Should I try again? (y/n)"? One env var kills it

On a corporate Windows box, git pull/git fetch keeps stopping to ask:

Unlink of file '.git/objects/pack/pack-305a05....idx' failed. Should I try again? (y/n)

The cause is a security agent — SentinelOne, ZScaler, Defender — holding an open handle on the old .idx files while Git tries to repack. Git for Windows wraps unlink/rename failures in a retry prompt, and you end up babysitting every pull, mashing n.

yes n | git pull works but you have to remember to prefix it every time. The permanent fix is one line in ~/.bashrc (the Git Bash one):

export GIT_ASK_YESNO=false

Git runs the value of GIT_ASK_YESNO as a command to decide whether to retry — a non-zero exit is treated as "n". false always exits non-zero, so every prompt is silently answered "no". It's cleaner than </dev/null redirection (works regardless of whether stdin is a tty) and doesn't touch the other interactive bits — commit-message editor, credential prompts — which go through different machinery.

Answering "n" just means Git leaves the locked old pack file on disk; the new pack is already live, so the repo is fine. Once the security agent lets go, a git gc sweeps up the leftovers.

If the prompts are frequent, this cuts down how often they fire — sometimes the lock is Git's own multi-threaded pack-objects, not the AV:

git config --global pack.threads 1

This is the silence-it companion to the heavier "it's an open handle, not a permission" diagnosis — same root cause (a process holding a handle), but here you just want the nagging to stop, not to hunt the locker down.

`claude install` says success on Windows but `--version` is still old

You run claude install, it prints "successfully installed! Version: 2.1.185", but claude --version keeps reporting the old 2.1.183. Reinstalling doesn't help.

The cause: on Windows you cannot overwrite a running .exe. The native installer downloads the new build into ~/.local/share/claude/versions/<ver> fine, but the final step — copying it onto the launcher at ~/.local/bin/claude.exe — fails silently because a live claude.exe process holds that file locked. The installer reports success on the download, not on the swap.

Confirm it's this by comparing checksums of the launcher against the version store:

sha256sum ~/.local/bin/claude.exe ~/.local/share/claude/versions/2.1.185
ls -la ~/.local/share/claude/versions/   # newest version is there, but bin/claude.exe is stale
tasklist //FI "IMAGENAME eq claude.exe"  # the processes holding the lock

If the launcher hash matches an older version in the store, the swap never happened.

The clean fix is to exit every claude session and re-run claude install. But if you can't (e.g. you're driving from inside a claude session), use the fact that Windows lets you rename a locked file even though it won't let you overwrite it — the running process keeps its open handle, and a fresh file lands in the path:

cd ~/.local/bin
mv claude.exe claude.exe.old
cp ~/.local/share/claude/versions/2.1.185 claude.exe
chmod +x claude.exe
claude --version   # 2.1.185

New shells immediately pick up the new binary; running sessions keep using the old one until restarted. Delete claude.exe.old once everything has been restarted.

Note that Git Bash's bare claude (no extension) is just shell resolution of claude.exe — there's only one physical file to replace, not two. This whole trap is more likely if you have autoUpdates: false in ~/.claude.json and update manually, since background auto-update would normally retry on the next launch when nothing is locked.

xset s off vs xset -dpms: Do you need both?

On Linux (especially XFCE, GNOME, or other X11 desktops), if you want to keep your monitors from auto-sleeping while still being able to manually turn them off with a hotkey, you'll see these two commands recommended:

xset s off
xset -dpms

Do you need both? Usually not—but it doesn't hurt. Here's the difference:

xset -dpms controls the monitor power management (hardware sleep). This is the main one you want—it prevents the monitor from entering standby/suspend/off automatically.

xset s off controls the X11 screen saver (software blanking). In modern desktops, the screen saver usually just triggers DPMS anyway, so this often feels redundant. But it's a good safety layer to prevent "blank screen but not actually sleeping" behavior on some setups.

The practical setup

To never auto-sleep, but still allow manual off/on:

xset s off        # Disable screen saver blanking
xset -dpms        # Disable DPMS power saving

Then bind this to a hotkey to turn monitors off:

xset dpms force off

Moving the mouse or pressing any key wakes them back up, and they stay awake afterward (because DPMS stays disabled).

XFCE note

Don't forget to also disable display power management in: Settings → Power Manager → Display

Otherwise XFCE might re-enable DPMS behind your back.

I just found a might better way: directly use xfce4-screensaver-command --lock for hot key Alt+Ctrl+L. Thus you don't need to worry about XFCE re-eableing.

Replacing tmux send-keys for agent coordination? stdin FIFO is the right answer, not an alternative

tmux send-keys became the backbone of multi-agent relay systems for one reason: it could inject input into a running AI agent process at any moment, simulating a human interrupt. It worked. But it was always a hack — it targets a terminal pane, not a process, and carries all of tmux's fragility on Windows Git Bash with it.

The cleaner architecture separates the two things tmux was doing simultaneously:

1. Process lifecycle: supervisor loop

Instead of an external process injecting /next-run <path> into a live agent's stdin, the agent writes its own handoff file before exiting:

# supervisor wrapper
handoff="/tmp/next-run-${session_id}.md"
printf 'poll\n' > "$handoff"
while [[ -f "$handoff" ]]; do
    claude-code "/next-run $handoff"
done

The agent finishes a cycle, writes the next handoff, and exits cleanly. The supervisor reads the file and restarts. No injection, no timing race, no tmux session required. The agent writes its last will before dying — context stays clean across restarts because each cycle starts fresh.

2. Async wake / interrupt: stdin FIFO + registration file

For the "prod is on fire, wake up the polling agent" case, give each agent a named pipe as its stdin:

fifo="/tmp/mux-agent-${slug}-${backend}-${mode}.fifo"
mkfifo "$fifo"
claude-code "/next-run $handoff" < "$fifo" &
printf '%s\t%s\n' "$$" "$fifo" > "/tmp/mux-agent-${slug}-${backend}-${mode}.reg"

Any process that wants to wake the agent writes to the FIFO — same semantics as send-keys, but at the OS level, no tmux required.

3. Discovery: the registration file is the registry

mux agents no longer needs to walk tmux list-panes -a. It scans /tmp/mux-agent-*.reg, checks kill -0 $pid for each entry, and lists the live ones. The FIFO path in the registration file is both the agent's address and its communication channel — one mechanism solves discovery and messaging.

What this unlocks

With display decoupled from communication, agents can run entirely in the background. Output goes to a log file; mux log <agent> tails it when you want to watch. Closing a terminal window no longer kills the agent. On locked-down Windows hosts without WSL2, the whole stack runs in Git Bash with no tmux dependency at all.

The tmux send-keys approach worked because it exploited the one interface AI coding agents expose: their terminal stdin. The FIFO approach exploits the same interface at the OS level — it's not a workaround, it's the same idea done properly.

The one thing tmux still does better is real-time pane-peeking across agents in team mode. For multi-agent workflows where one agent needs to interrupt another mid-execution, stdin FIFO is the right replacement. For single-agent lifecycle management, the supervisor loop makes the interrupt unnecessary in the first place.

Need a per-repo toggle for your own tool? Stash it in `git config`

When you build tooling around git repos, you eventually hit "where do I store a per-repo setting?" Committing a .toolrc pollutes the repo and needs a PR; a sidecar file in ~/.config can't easily be per-repo. The clean answer: put it straight into git config under your own namespace.

# per-repo (lands in <repo>/.git/config — local, never committed)
git config --local mux.adhocWorktree false

# per-machine, all repos (lands in ~/.gitconfig)
git config --global mux.adhocWorktree false

# read it back, normalized to true/false
git config --bool --get mux.adhocWorktree   # -> false ; exit 1 if unset

git lets you invent any <section>.<key> it doesn't recognize (mux.* here) and just stores it. That hands you three things for free that you'd otherwise have to build:

  • A per-repo store that never leaks.git/config is local to the checkout and never committed, so it won't pollute someone else's repo.
  • Scope layering with native precedence--local (repo) transparently overrides --global (machine). One git config --get resolves the hierarchy; you don't write any "check repo, then fall back to machine" logic.
  • Type parsing--bool normalizes false/0/no/offfalse, and a non-zero exit cleanly signals "not configured" so you can fall through to your built-in default.

So a full precedence chain for a feature flag becomes: CLI flag → env var → git config (local→global) → built-in default — and the middle two scopes are entirely git's job.

One gotcha with worktrees: git config --local writes to the shared common .git/config, not the linked worktree, because worktrees share config. If your setting is meant to key off the main checkout (it usually is), that's exactly right — but don't expect two worktrees of the same repo to hold different --local values.

The decision that surfaced this: an agent launcher was hardwiring a git worktree per session (hundreds of MB on big repos, painful on a VM). The whole "make it opt-out, but where does the per-repo marker live?" problem evaporated the moment we realized git config already is a per-repo + per-machine settings store with precedence baked in.