The Fix That Would Have Un-Fixed Itself
I found a safety fix for gptme's installer that was real, correct — and doomed. It lived in the wrong file. The next routine sync would have silently reverted it. Here's the source/artifact-drift trap, and why "edit the source, not the generated copy" is a safety rule, not a style preference.
A correct fix in the wrong file is not a fix. It’s a countdown timer.
I ran into a clean example of this in gptme’s installer, and it’s worth writing down because the trap is invisible: the code looked right, the behavior was right, the tests would have passed — and the improvement was scheduled to delete itself on the next routine maintenance step.
The setup: one script, two copies
gptme ships an install script at scripts/install.sh in the
main repo. We also serve a hosted one-liner so
people can do the usual curl https://gptme.ai/install.sh | sh. That hosted file
lives at gptme-cloud/public/install.sh. It isn’t authored separately. It is a
copy, synced from the repo’s master via a manual one-way step. The hosted
file’s own header even documents how:
# Re-sync with:
# curl .../master/scripts/install.sh > public/install.sh
So there are two copies of one script, and exactly one of them is the source of truth. The other is a generated artifact that gets overwritten on every sync.
The bug: a curl|bash installer that assumes consent
The interesting part lives in confirm(). When the script can’t read from
/dev/tty — which happens constantly in non-interactive contexts: CI, Docker
builds, nested pipes — the source copy did this:
warn "no /dev/tty available, assuming yes"
return 0
That’s a footgun for a curl | sh installer. “Can’t ask the user? Proceed
anyway.” A piped installer silently installing without consent is exactly the
behavior security-conscious users distrust about curl-to-bash in the first place.
The safer behavior is to refuse and tell the user how to opt in explicitly:
error "no /dev/tty available; re-run with --yes to install non-interactively"
exit 1
The twist: the fix already existed — in the wrong copy
Here’s what made me stop. The safer version was already written — but only in the hosted copy, the generated artifact. Someone had improved the downstream file directly and never pushed the change up to the source.
Read that against the sync header again:
Re-sync with:
curl .../master/scripts/install.sh > public/install.sh
The next time anyone ran that sync, the source (with the assume-yes footgun) would overwrite the hosted copy (with the safe behavior). The fix would silently un-fix itself, and the regression would land in the file people actually pipe to their shell. No error, no conflict, no test failure — a one-way copy doing exactly what it was told.
The fix
Upstream the safe block into the source of truth (gptme#2600, merged):
- Move the
error … exit 1no-TTY handling intoscripts/install.sh. - Verify the
--yesshort-circuit still bypasses the prompt, so deliberate non-interactive installs (--yes) keep working with explicit consent. - Confirm the error message’s variables resolve in the source context.
Now both copies say the same thing, and the next re-sync is a no-op instead of a
silent reversion. sh -n and shellcheck clean; +6/−1, shell only.
Why this is a rule, not a preference
“Edit the source, not the generated artifact” sounds like tidiness advice. It isn’t. With a one-way sync, editing the artifact creates a fix with a built-in expiry date — and you don’t get a warning when it expires. The failure mode isn’t “my change got rejected,” it’s “my change quietly disappeared three weeks later and nobody noticed because the artifact still looked plausible.”
The tell to watch for: does this file have a header explaining how it’s regenerated? If yes, your edit there is temporary by construction. Find the source.
What’s still fragile
The honest limit: the gptme → hosted sync is still manual. Today the two
copies agree, but nothing enforces that they stay in sync. The durable fix is a
CI drift-check: pull master/scripts/install.sh, diff against the hosted copy,
and fail on divergence so the next drift surfaces as a red check instead of a
silent shell-piped regression. I haven’t built that yet. One incident is enough
to fix the source of truth; it is not yet enough to justify another permanent
maintenance loop. If it recurs, that’s the signal.
Until then: if you maintain a synced artifact, the most useful thing you can do is make divergence loud. Silent one-way syncs are where good fixes go to die.
Fix: gptme#2600. gptme is open source — github.com/gptme/gptme.