feat: automatic git-config self-heal in helpers4-common, opt-in SSH keys in dotfiles-sync - #74
Closed
baxyz wants to merge 4 commits into
Closed
feat: automatic git-config self-heal in helpers4-common, opt-in SSH keys in dotfiles-sync#74baxyz wants to merge 4 commits into
baxyz wants to merge 4 commits into
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>
…dant path-fixing Two changes, same underlying cause: helpers4-common's new git-config-self-heal.sh (postAttachCommand) now fixes host-specific .gitconfig paths for every consumer automatically, regardless of whether dotfiles-sync is even in use — this feature no longer needs to solve that problem itself, and syncing the actual key file this feature's own signingkey rewrite existed to support is no longer the only way to get a working signing key either. - SSH private/public key file copying is now gated behind a new syncSshKeys option, default false. ~/.ssh/config and known_hosts still sync unconditionally — agent forwarding doesn't provide either of those — but actual key files no longer land on the container's filesystem unless explicitly requested. Normal SSH auth (git clone/push over SSH, ssh to a remote host) already works through the client's own forwarded ssh-agent with no local key file needed at all; copying them by default put private key material on the container's disk for no benefit in the common case, directly against what setups relying purely on agent forwarding deliberately avoid. - Removed this feature's own .gitconfig path-rewriting and verification (path-keys.sh, REHOMEABLE_PATH_KEYS/ VERIFY_ONLY_PATH_KEYS) — helpers4-common's self-heal actively fixes the same class of problem (and more: it also handles credential.helper/gpg.program pointing at a moved binary, which this feature's own version only ever warned about, never fixed), for every consumer, not just this feature's own users. Keeping both would mean two copies of the same "is this host path still valid" logic to maintain. - Now depends on helpers4-common (shares its h4_detect_cloud_env instead of an inline duplicate of the same Codespaces/Gitpod/ DevPod/WSL detection). Verified end-to-end in real Docker containers: default config skips key files while still merging config/known_hosts; enabling syncSshKeys places the key files, and helpers4-common's self-heal correctly finds the locally-synced .pub by basename to repair a still-host-shaped signingkey path. Version bumped to 1.1.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the new helpers4-common/dotfiles-sync split in AGENTS.md's "Design constraints for features" section: why this lives in helpers4-common (the one dependency every feature already has, so it runs for every consumer automatically with nothing to add) rather than a dedicated feature or dotfiles-sync, 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, not a plain set. Also updates the feature table to the new 1.1.0 versions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
✅ PR Validation Passed
📋 Pipeline Status
🤖 Generated by @helpers4 CI • 2026-09-06 |
…tch in Test 8b The default "username" option (node) doesn't necessarily match the base image's actual default process user (e.g. "vscode" on an mcr.microsoft.com/devcontainers/base image) when a CI matrix entry doesn't override it — test.sh itself then can't write into TARGET_HOME, unrelated to anything the test is actually checking. Skip the write-dependent assertions with a WARN in that case instead of hard-failing, matching the tolerant style Test 8 already uses for the same class of test-environment limitation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4 tasks
Contributor
Author
|
Splitting this into two PRs, per discussion — see #75 for the reasoning (the
Closing this one in favor of those. |
3 tasks
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
Fixes a real production incident (missing SSH-format commit signing key + a stale
credential.helperpointing at a since-movedghbinary) and generalizes the fix into permanent infrastructure, per discussion.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 host-specific path might not resolve inside this particular container:Neither is caused by (or fixable in) a single feature — this is a client-copy problem, so the fix has to run after the client's own copy has happened, for every consumer, not just one project that remembered to configure something.
helpers4-common(feat, 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), shared withdotfiles-sync.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.dotfiles-sync(refactor, 1.0.8 → 1.1.0)syncSshKeys, defaultfalse).~/.ssh/configandknown_hostsstill sync unconditionally (agent forwarding doesn't provide either). Normal SSH auth already works through the client's forwarded agent with no local key file needed — copying them by default put private key material on the container's disk against what agent-forwarding-only setups deliberately avoid..gitconfigpath-rewriting/verification (path-keys.sh) —helpers4-common's self-heal now does the same job, better (actively fixescredential.helper/gpg.program, not just warns), for every consumer regardless of whetherdotfiles-syncis in use.dependsOn: helpers4-common.Docs
dev.containers.copyGitConfigwhen usingdotfiles-sync, since it currently wins silently over that feature's smarter merge).AGENTS.md: version bumps + a new "Design constraints" entry on why this lives inhelpers4-commonand thegit configmulti-value gotcha for future feature authors.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)dotfiles-syncdefault config skips key files while still mergingconfig/known_hosts;syncSshKeys=trueplaces them;helpers4-common's self-heal correctly finds a locally-synced.pubby basename to repair a still-host-shapedsigningkeypathdevcontainer features testbuild phase for both features, confirmingdependsOn: helpers4-commonresolves correctly (launch itself hits the same docker-outside-of-docker sandbox limitation as prior PRs, unrelated to this change)test/helpers4-common/test.shandtest/dotfiles-sync/test.shupdated and passing, including new functional assertions for the self-heal and thesyncSshKeysgating🤖 Generated with Claude Code