Skip to content

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
mainfrom
feat/git-config-self-heal
Closed

feat: automatic git-config self-heal in helpers4-common, opt-in SSH keys in dotfiles-sync#74
baxyz wants to merge 4 commits into
mainfrom
feat/git-config-self-heal

Conversation

@baxyz

@baxyz baxyz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a real production incident (missing SSH-format commit signing key + a stale credential.helper pointing at a since-moved gh binary) 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:

signingkey = ~/.ssh/id_baxyz_craft_ed25519.pub   # never existed in the container
[credential "https://gist.github.com"]
    helper = !/snap/gh/751/gh auth git-credential  # gh lives at /usr/local/bin/gh here

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 on helpers4-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=ssh only): tries a same-basename file under ~/.ssh/~/.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 — 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_env to common.sh (Codespaces/Gitpod/DevPod/WSL detection), shared with dotfiles-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 plain git config <key> <value> refuse outright (exit 5). The first implementation swallowed that failure via 2>/dev/null || true while 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)

  • SSH private/public key file copying is now opt-in (syncSshKeys, default false). ~/.ssh/config and known_hosts still 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.
  • Removed this feature's own .gitconfig path-rewriting/verification (path-keys.sh) — helpers4-common's self-heal now does the same job, better (actively fixes credential.helper/gpg.program, not just warns), for every consumer regardless of whether dotfiles-sync is in use.
  • Now dependsOn: helpers4-common.

Docs

  • Both READMEs updated (options, version history, a note recommending disabling VS Code's dev.containers.copyGitConfig when using dotfiles-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 in helpers4-common and the git config multi-value gotcha for future feature authors.

Test plan

  • bash -n / jq empty on all changed scripts and manifests
  • Real Docker end-to-end: reproduced the exact reported .gitconfig (stale gh path + 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)
  • Real Docker end-to-end: dotfiles-sync default config skips key files while still merging config/known_hosts; syncSshKeys=true places them; helpers4-common's self-heal correctly finds a locally-synced .pub by basename to repair a still-host-shaped signingkey path
  • devcontainer features test build phase for both features, confirming dependsOn: helpers4-common resolves correctly (launch itself hits the same docker-outside-of-docker sandbox limitation as prior PRs, unrelated to this change)
  • test/helpers4-common/test.sh and test/dotfiles-sync/test.sh updated and passing, including new functional assertions for the self-heal and the syncSshKeys gating
  • Verified the Codespaces-secrets doc URL referenced in the warning message resolves to a real, current GitHub docs page

🤖 Generated with Claude Code

baxyz and others added 3 commits September 6, 2026 15:07
…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>
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

✅ PR Validation Passed

All checks passed!


📋 Pipeline Status

Job Status
🧾 Conventional Commits passing
🔖 Version Bump passing
🧪 Feature Tests passing
🐚 ShellCheck passing

🤖 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>
@baxyz

baxyz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Splitting this into two PRs, per discussion — see #75 for the reasoning (the helpers4-common half needs to actually merge and publish to GHCR before dotfiles-sync's own CI test can correctly pick up the new h4_detect_cloud_env function via dependsOn, since that resolves from the published registry, not this repo's local src/ tree).

Closing this one in favor of those.

@baxyz baxyz closed this Sep 6, 2026
@baxyz
baxyz deleted the feat/git-config-self-heal branch September 8, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant