Skip to content

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 your devcontainer.json yourself.

common.sh defines nine shell functions, sourced by features that need them:

FunctionPurpose
h4_require_rootPrints 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_userResolves 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_homeResolves that user’s home directory (/root or the passwd entry, falling back to /home/<user>)
h4_apt_updateRuns apt-get update once, skipped if the apt lists cache is already populated
h4_ensure_packagesInstalls only the packages from its argument list that aren’t already present, running h4_apt_update first if needed
h4_detect_cloud_envSets 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_tripleMaps 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)

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 when gpg.format=ssh) — if the file is missing, tries a same-basename file under ~/.ssh/~/.gnupg first (covers a case like dotfiles-sync having 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 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, 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.

A feature that needs these helpers depends on this feature instead of inlining its own copy:

{
    "dependsOn": {
        "ghcr.io/helpers4/devcontainer/helpers4-common:1": {}
    }
}

Then, in install.sh:

# shellcheck source=/dev/null
source /usr/local/share/helpers4/common.sh

h4_detect_user
h4_resolve_home
h4_ensure_packages jq curl

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.

  • v1.2.3: Added h4_require_root. Thirteen install.sh scripts each carried their own id -u check, 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-absorb and bitwarden-secrets-manager: h4_arch_musl_triple (maps uname -m to the x86_64-unknown-linux-musl / aarch64-unknown-linux-musl target-triple convention those two features’ Rust binaries use) and h4_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 — see bitwarden-secrets-manager’s bws- 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 by nub, vite-plus, claude-dev) was evaluated too, but each invocation differs in ways that matter (fatal vs. degraded failure, different su argument-passing styles, different env vars) — forcing a shared wrapper risked exactly the kind of silent behavior change already found and fixed once in vite-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, and include.path/includeIf.*.path pointing at a file or directory missing in this container — none of those have a $PATH search or forwarded-agent equivalent to recover from, so this only names the gap. Fixed the core.editor/core.sshCommand listing above, which had drifted from the actual shell-out key list since core.sshCommand was 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 --shared case (chown-once vs. chmod-to-share) that only claude-dev/mistral-dev had. 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 (alongside credential.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_env and an automatic postAttachCommand git-config self-heal (see above) — fixes host-specific paths a client’s automatic .gitconfig copy or SSH agent forwarding leaves broken. Every helpers4 feature now depends on helpers4-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 jq installation out of this feature and into the individual features that actually need it — helpers4-common itself 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.