feat(helpers4-common): automatic git-config self-heal on every attach - #75
Merged
Conversation
…attach A client's own automatic behavior — VS Code copying ~/.gitconfig, SSH agent forwarding — happens outside any devcontainer Feature's control, verbatim, with no awareness that a host-specific path might not resolve inside this particular container: a credential.helper shelling out to a snap-managed `gh` at a revision-pinned path that doesn't exist here, or a gpg.format=ssh user.signingkey pointing at a public key file that only ever existed on the host. Adds postAttachCommand: git-config-self-heal.sh, which runs for every helpers4 consumer automatically (no opt-in — every feature already depends on helpers4-common since the dependsOn migration): - credential.helper (incl. per-URL scopes), gpg.program, gpg.ssh.program, core.editor: when the value shells out to an absolute path that doesn't resolve here, rewritten to the bare command name once a same-named binary is found on $PATH. Bare, not a fresh absolute path: it never goes stale again even if the tool moves on a future rebuild. - user.signingkey (gpg.format=ssh only): tries a same-basename file under ~/.ssh or ~/.gnupg first, then recovers the public key live from a forwarded ssh-agent matched against user.email (ssh-add -L only, never touches private key material). On GitHub Codespaces, which doesn't forward a local ssh-agent at all, warns with a pointer to Codespaces secrets and notes Codespaces signs GPG-format commits natively via its own managed proxy instead. Also adds h4_detect_cloud_env to common.sh (Codespaces/Gitpod/DevPod/ WSL detection), shared with dotfiles-sync's own detection. Found and fixed a real bug while testing against a multi-valued credential.helper (a blank "reset" entry followed by a real one, a legitimate and common pattern): plain `git config <key> <value>` refuses outright (exit 5) the moment a key already has more than one value, and the original _set implementation swallowed that failure via `2>/dev/null || true` while still reporting success. Fixed by targeting the one broken value specifically via `--replace-all <key> <new> <old-as-anchored-regex>` and actually checking the exit status before claiming a fix. Verified end-to-end against the exact real .gitconfig snippet that prompted this (credential.helper pointing at a since-moved snap gh binary, gpg.format=ssh with a missing signingkey) in real Docker containers, plus the multi-value regression case, both warning paths (nothing resolvable, and the GitHub Codespaces-specific message), and idempotence (a clean second run reports nothing left to fix). Version bumped to 1.1.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ting gotcha Records why the automatic postAttachCommand self-heal lives in helpers4-common rather than a dedicated feature (the one dependency every feature already has, so it runs for every consumer automatically), and the git config multi-value gotcha (`git config <key> <value>` refuses outright the moment a key already has more than one value) any future config-writing self-heal logic needs to route around via --replace-all with an anchored value pattern. Also documents a real testing-infrastructure discovery made while building this: `devcontainer features test --features <consumer> .` resolves a `dependsOn` reference from the published GHCR registry, not this repo's local src/ tree — only the feature passed directly via --features gets freshly-built local content. A consumer whose test needs brand-new, not-yet-published behavior from a dependency will fail CI until that dependency's own version bump actually merges and releases first — not a bug in the consumer, a limitation of testing interdependent changes within a single PR. This is why the dotfiles-sync half of this work is a separate, follow-up PR rather than bundled with this one. Updates the feature table to helpers4-common 1.1.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closed
6 tasks
✅ PR Validation Passed
📋 Pipeline Status
🤖 Generated by @helpers4 CI • 2026-09-06 |
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First of two PRs (split from #74 — see below for why). This one is
helpers4-commononly.The root cause
A client's own automatic behavior — VS Code copying
~/.gitconfig, SSH agent forwarding — happens outside any devcontainer Feature's control, verbatim, with no awareness that a path baked into the host's config might not resolve inside this particular container:helpers4-common(1.0.1 → 1.1.0)New
postAttachCommand: git-config-self-heal.sh— automatic for every consumer, since every feature already depends onhelpers4-common:credential.helper(incl. per-URL scopes),gpg.program,gpg.ssh.program,core.editor: a shell-out to an absolute path that doesn't resolve here gets rewritten to the bare command name once a same-named binary is found on$PATH— bare, not a fresh absolute path, so it never goes stale again even if the tool moves on a future rebuild.user.signingkey(gpg.format=sshonly): tries a same-basename file under~/.ssh/~/.gnupgfirst, then recovers the public key live from a forwardedssh-agentmatched againstuser.email(ssh-add -Lonly, never touches private key material). On GitHub Codespaces, which doesn't forward a localssh-agentat all — confirmed via GitHub's own community discussion, an official response states this explicitly — warns with a pointer to Codespaces secrets instead of silently doing nothing.Also adds
h4_detect_cloud_envtocommon.sh(Codespaces/Gitpod/DevPod/WSL detection).A real bug found and fixed while testing: a multi-valued
credential.helper(a blank "reset" entry followed by a real one — legitimate, common) makes plaingit config <key> <value>refuse outright (exit 5). The first implementation swallowed that failure via2>/dev/null || truewhile still reporting success. Fixed by targeting the one broken value via--replace-all <key> <new> <old-as-anchored-regex>and actually checking the exit status.Why split from the original combined PR (#74)
#74 also included a
dotfiles-syncrefactor that depends onh4_detect_cloud_env. While testing, discovered thatdevcontainer features test --features dotfiles-sync .resolvesdependsOn: ghcr.io/helpers4/devcontainer/helpers4-common:1from the published GHCR registry, not this repo's localsrc/tree — confirmed by inspecting the actual staged build content (missinggit-config-self-heal.shentirely) and the published manifest (docker manifest inspect ghcr.io/helpers4/devcontainer/helpers4-common:1→ still1.0.1). Only the feature passed directly via--featuresgets fresh local content; a transitive dependency's CI test will fail if it needs behavior that isn't published yet — not a bug in the consumer, a limitation of testing interdependent changes within one PR. Landinghelpers4-commonhere first, thendotfiles-syncin a follow-up once this is merged and released, sidesteps it entirely. Documented inAGENTS.mdfor next time.#74 will be closed in favor of this PR + the dotfiles-sync follow-up.
Test plan
bash -n/jq emptyon all changed scripts and manifests.gitconfig(staleghpath + missing SSH signing key), ran the self-heal, confirmed both fixed; confirmed idempotence (clean second run); confirmed both warning paths (nothing resolvable locally, and the GitHub Codespaces-specific message)test/helpers4-common/test.shupdated with functional assertions for the self-heal, passing in a real container🤖 Generated with Claude Code