helpers4 Common Library
Code name:
helpers4-common
Shared bootstrap library for helpers4 DevContainer features. It installs a single
/usr/local/share/helpers4/common.sh script providing user detection, home directory
resolution, and apt package helpers — logic that most other helpers4 features need and
used to each carry their own inline copy of.
Note: This is an internal dependency, not a feature you install directly. Other helpers4 features pull it in automatically via
dependsOn— you won’t normally add it to yourdevcontainer.jsonyourself.
What it provides
Section titled “What it provides”common.sh defines nine shell functions, sourced by features that need them:
| Function | Purpose |
|---|---|
h4_require_root | Prints the standard “must be run as root” message to stderr and exits 1 unless the script runs as root — call it right after sourcing common.sh, in place of a hand-written id -u check |
h4_detect_user | Resolves the target container user (vscode, node, codespace, the first UID 1000 account, or root), respecting USERNAME/_REMOTE_USER and the auto/automatic/none conventions |
h4_resolve_home | Resolves that user’s home directory (/root or the passwd entry, falling back to /home/<user>) |
h4_apt_update | Runs apt-get update once, skipped if the apt lists cache is already populated |
h4_ensure_packages | Installs only the packages from its argument list that aren’t already present, running h4_apt_update first if needed |
h4_detect_cloud_env | Sets IS_CLOUD_ENV (true/false) and ENV_LABEL (GitHub Codespaces, Gitpod, DevPod, WSL, or local) by checking the well-known env vars each platform sets |
h4_ensure_volume_writable <path> [--shared] | Hands a root-owned named volume to the current user. Without --shared: always chown (safe for a volume exclusive to one container). With --shared: chown only while still root-owned, otherwise chmod o+rwX instead of stealing ownership from another concurrently-running project’s container |
h4_arch_musl_triple | Maps uname -m to the x86_64-unknown-linux-musl / aarch64-unknown-linux-musl target-triple convention used by Rust-built CLI releases (git-absorb, bitwarden-secrets-manager) — echoes the triple, or writes an error to stderr and returns 1 on an unsupported architecture |
h4_github_latest_tag <owner/repo> [prefix] | Resolves a GitHub repo’s latest release tag via the REST API — no prefix queries /releases/latest directly; with a prefix, scans the full /releases list for the first tag starting with it (for a monorepo publishing several tools under one repo, like bitwarden/sdk-sm’s bws- releases) — echoes the tag, or nothing on failure or no match (caller checks for an empty result) |
Automatic git-config self-heal
Section titled “Automatic git-config self-heal”This feature also installs /usr/local/share/helpers4/git-config-self-heal.sh, wired up as a
postAttachCommand — it runs on every attach, for every helpers4 consumer, with nothing to
configure. No opt-in, because there’s nothing here that isn’t already broken without it.
Why: a client’s own automatic ~/.gitconfig copy (VS Code does this by default) and its SSH
agent forwarding both happen outside any devcontainer Feature’s control, verbatim, with no
awareness that a path baked into the host’s config might not resolve inside this specific
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.
What it fixes, generically (no per-tool/per-feature knowledge baked in, so it doesn’t go stale as installed tools move around):
credential.helper(including per-URL scopes),gpg.program,gpg.ssh.program,core.editor,core.sshCommand— when the value shells out to an absolute path that doesn’t resolve here, it’s rewritten to the bare command name once a same-named binary is found on$PATH. Bare, not a freshly-resolved absolute path again: it never goes stale a second time even if the tool moves on a future rebuild.user.signingkey(only whengpg.format=ssh) — if the file is missing, tries a same-basename file under~/.ssh/~/.gnupgfirst (covers a case likedotfiles-synchaving already placed the real file under a different absolute path than the host’s), then falls back to recovering the public key live from a forwardedssh-agent, matched againstuser.email(ssh-add -Lonly — never touches private key material). On GitHub Codespaces, which doesn’t forward a localssh-agentat all, this can’t be derived automatically — the warning points at Codespaces secrets and notes that Codespaces signs GPG-format commits natively via its own managed proxy, as an alternative.
What it can only flag, never fix: core.hooksPath, core.excludesfile,
core.attributesfile, and include.path/includeIf.*.path all point at a file or directory
that only ever existed on the host, with no $PATH search or forwarded-agent equivalent to fall
back to — a warning names the gap, nothing more. git config --file also never follows
includes, so an included file’s own contents (if it even exists) stay invisible to every check
above; this can only confirm whether the included file itself is present.
Anything it can’t fix itself is a warning, never a failure — it never blocks the attach.
Usage (for feature authors)
Section titled “Usage (for feature authors)”A feature that needs these helpers depends on this feature instead of inlining its own copy:
Then, in install.sh:
helpers4-common has no options — there is nothing to configure. The git-config self-heal
above is not opt-in either; it runs for every consumer automatically.
Why a shared feature instead of a copy per feature
Section titled “Why a shared feature instead of a copy per feature”Before this feature existed, every helpers4 feature that needed user detection or apt
helpers carried an inline copy of the same ~30 lines of bash. Keeping N copies in sync
by hand doesn’t scale, and a fix applied to one copy silently doesn’t reach the others.
helpers4-common is the single source of truth: fix h4_ensure_packages once here, and
every dependent feature picks it up on its next install.
Version History
Section titled “Version History”- v1.2.3: Added
h4_require_root. Thirteeninstall.shscripts each carried their ownid -ucheck, with five different wordings of the error message; features can now call this one instead. Nothing changes for existing features until they are migrated to it. - v1.2.2: Added two shared helpers, extracted from duplicated logic in
git-absorbandbitwarden-secrets-manager:h4_arch_musl_triple(mapsuname -mto thex86_64-unknown-linux-musl/aarch64-unknown-linux-musltarget-triple convention those two features’ Rust binaries use) andh4_github_latest_tag <owner/repo> [prefix](resolves a GitHub repo’s latest release tag, optionally filtered by tag prefix for a monorepo that publishes several tools under one repo — seebitwarden-secrets-manager’sbws-filter).github-dev’s own arch mapping (amd64/arm64/armv6, GitHub CLI’s own asset-naming convention) is a different scheme entirely, not a duplicate of the musl triple above, so it stays feature-local. An “official installer” helper (curl-installer scripts used bynub,vite-plus,claude-dev) was evaluated too, but each invocation differs in ways that matter (fatal vs. degraded failure, differentsuargument-passing styles, different env vars) — forcing a shared wrapper risked exactly the kind of silent behavior change already found and fixed once invite-plus’s own installer call, so left as feature-local. - v1.2.1: The git-config self-heal now also warns (never fixes) about
core.hooksPath,core.excludesfile,core.attributesfile, andinclude.path/includeIf.*.pathpointing at a file or directory missing in this container — none of those have a$PATHsearch or forwarded-agent equivalent to recover from, so this only names the gap. Fixed thecore.editor/core.sshCommandlisting above, which had drifted from the actual shell-out key list sincecore.sshCommandwas added. - v1.2.0: Added
h4_ensure_volume_writable, extracted from four features (pnpm-store,playwright-dev,claude-dev,mistral-dev) that each carried their own copy of the same named-volume ownership logic — including the subtler--sharedcase (chown-once vs. chmod-to-share) that onlyclaude-dev/mistral-devhad. One correct implementation instead of four independently-maintained copies. - v1.1.1: Extended the git-config self-heal’s shell-out key list to
core.sshCommand(alongsidecredential.helper,gpg.program,gpg.ssh.program,core.editor) — the same class of host-baked absolute path, previously left unhandled. - v1.1.0: Added
h4_detect_cloud_envand an automaticpostAttachCommandgit-config self-heal (see above) — fixes host-specific paths a client’s automatic.gitconfigcopy or SSH agent forwarding leaves broken. Every helpers4 feature now depends onhelpers4-common(the repo-wide migration off each feature’s own inlined bootstrap copy), so this runs for every consumer automatically, with nothing to add or configure. - v1.0.1: Moved
jqinstallation out of this feature and into the individual features that actually need it —helpers4-commonitself no longer installs any packages, only the shell functions. - v1.0.0: Initial extraction of the shared bootstrap (user detection, home resolution, apt helpers) out of the features that used to inline it.
