Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ defaults:
shell: bash -euo pipefail {0}

jobs:
verify-material:
name: Verify vendored checksums
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install minisign
run: |
sudo apt-get update -y
sudo apt-get install -y --no-install-recommends minisign

- name: Verify vendored checksums
run: bash scripts/verify-material.sh

discover:
name: Discover features
runs-on: ubuntu-latest
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ signing material committed to this repository, and requires no other Feature.
| Feature | What you get | Source and verification |
|---------|--------------|-------------------------|
| [claude-code](src/claude-code)<br>`ghcr.io/bare-devcontainer/features/claude-code` | Claude Code, Anthropic's agentic coding CLI, with the VS Code extension requested alongside it. Credentials, settings and history live on a volume, so a rebuild does not mean signing in again. | Anthropic's APT repository. The package signature is checked with a key committed to this repository. |
| [mise](src/mise)<br>`ghcr.io/bare-devcontainer/features/mise` | mise, the polyglot tool and runtime manager, for the toolchains and CLIs a project pins itself. | [GitHub Releases](https://github.com/jdx/mise/releases). The download is checked against upstream's own checksum file, committed to this repository with the minisign signature published for it. |
| [node](src/node)<br>`ghcr.io/bare-devcontainer/features/node` | Node.js, with Corepack in place of npm, so the package manager comes from the project's `packageManager` field rather than from the image. | [nodejs.org](https://nodejs.org/dist/). The release checksums are checked against the Node.js release keys committed to this repository. |

Each Feature's README documents its options, requirements, what it deliberately leaves
Expand Down Expand Up @@ -97,8 +98,9 @@ no key is fetched while the container is built, and no other Feature's install s
runs on its behalf.

**What you can check before pinning.** Every input is a file in this repository: one
install script per Feature, and the signing material sitting next to it. Each Feature's
README shows how to inspect its key.
install script per Feature, and the signing material sitting next to it — a signing key, or
upstream's own checksum file with its signature. Each Feature's README shows how to
inspect what it verifies against.

**How the trusted material changes.** Only through a pull request. The
`Update Trusted Material` workflow
Expand Down
67 changes: 67 additions & 0 deletions scripts/pin-checksums.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
#
# pin-checksums.sh — re-pin a feature's vendored release checksums to a named
# upstream release. Downloads the checksum file and the signature upstream
# published for it, and writes both into the working tree only once the
# signature verifies against the vendored public key, and sets the feature
# version to the release version. Performs no git operations.
#
# Which file belongs to which feature is listed in signed-material.json next to
# this script.
#
# Requires: minisign, jq, wget
#
# Usage:
# pin-checksums.sh <feature> <tag>
#
# Example:
# pin-checksums.sh mise v2026.9.11
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MATERIAL="${SCRIPT_DIR}/signed-material.json"

if [ "$#" -ne 2 ]; then
sed -n '/^# Usage:/,/^# *pin-checksums.sh <feature> <tag>/p' "${BASH_SOURCE[0]}" >&2
exit 2
fi
feature="$1"
tag="$2"

entry="$(jq -e --arg feature "$feature" '.[] | select(.feature == $feature)' "$MATERIAL")" || {
echo "(!) ${MATERIAL} has no entry for '${feature}'. Known: $(jq -r '[.[].feature] | join(", ")' "$MATERIAL")" >&2
exit 1
}

repository="$(jq -r '.repository' <<< "$entry")"
checksums="$(jq -r '.checksums' <<< "$entry")"
signature="$(jq -r '.signature' <<< "$entry")"
key="$(jq -r '.key' <<< "$entry")"
releases="https://github.com/${repository}/releases/download/${tag}"

tmp_checksums=$(mktemp)
tmp_signature=$(mktemp)
tmp_metadata=$(mktemp)
trap 'rm -f "$tmp_checksums" "$tmp_signature" "$tmp_metadata"' EXIT

echo "Downloading ${repository} ${tag} checksums" >&2
wget -q -T 30 -t 3 -O "$tmp_checksums" "${releases}/$(basename "$checksums")"
wget -q -T 30 -t 3 -O "$tmp_signature" "${releases}/$(basename "$signature")"

echo "Verifying the signature against ${key}" >&2
minisign -V -m "$tmp_checksums" -x "$tmp_signature" -p "$key"

if cmp -s "$tmp_checksums" "$checksums" && cmp -s "$tmp_signature" "$signature"; then
echo "${checksums} already pins ${tag}; nothing to do." >&2
exit 0
fi

chmod 644 "$tmp_checksums" "$tmp_signature"
mv "$tmp_checksums" "$checksums"
mv "$tmp_signature" "$signature"

metadata="src/${feature}/devcontainer-feature.json"
jq --indent 4 --arg version "${tag#v}" '.version = $version' "$metadata" > "$tmp_metadata"
cat "$tmp_metadata" > "$metadata"

echo "Pinned ${checksums} and ${metadata} to ${tag}." >&2
9 changes: 9 additions & 0 deletions scripts/signed-material.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"feature": "mise",
"repository": "jdx/mise",
"checksums": "src/mise/SHASUMS256.txt",
"signature": "src/mise/SHASUMS256.txt.minisig",
"key": "src/mise/mise-minisign.pub"
}
]
7 changes: 7 additions & 0 deletions scripts/update-material.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# to stdout depending on whether anything changed; per-file download and
# comparison progress goes to stderr. Performs no git or GitHub operations.
#
# Covers material published at a fixed URL, which is every signing key. Release
# checksums move with the release they pin, so pin-checksums.sh handles those.
#
# Usage:
# update-material.sh
set -euo pipefail
Expand All @@ -15,6 +18,10 @@ MATERIALS='[
"path": "src/claude-code/claude-code.asc",
"url": "https://downloads.claude.ai/keys/claude-code.asc"
},
{
"path": "src/mise/mise-minisign.pub",
"url": "https://raw.githubusercontent.com/jdx/mise/main/minisign.pub"
},
{
"path": "src/node/node-keyring.kbx",
"url": "https://github.com/nodejs/release-keys/raw/HEAD/gpg/pubring.kbx"
Expand Down
32 changes: 32 additions & 0 deletions scripts/verify-material.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
#
# verify-material.sh — check that the release checksums a feature's install
# script verifies downloads against are the ones upstream signed. Reads the
# working tree only, so it needs no network.
#
# The files covered, per feature, are listed in signed-material.json next to
# this script; pin-checksums.sh writes them.
#
# Requires: minisign, jq
#
# Usage:
# verify-material.sh
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MATERIAL="${SCRIPT_DIR}/signed-material.json"

count=0
while IFS=$'\t' read -r feature checksums signature key; do
echo "${feature}: verifying ${checksums} against ${signature} with ${key}" >&2
minisign -V -m "$checksums" -x "$signature" -p "$key"
count=$((count + 1))
done < <(jq -r '.[] | [.feature, .checksums, .signature, .key] | @tsv' "$MATERIAL")

# An empty table would otherwise report success without having checked anything.
if [ "$count" -eq 0 ]; then
echo "(!) ${MATERIAL} lists no material to verify." >&2
exit 1
fi

echo "Verified vendored checksums for ${count} feature(s)." >&2
121 changes: 121 additions & 0 deletions src/mise/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
The official release binary from [GitHub Releases](https://github.com/jdx/mise/releases)
is installed as `/usr/local/bin/mise`.

## Requirements

- A `linux-x64` or `linux-arm64` image. The statically linked musl build is the one
installed, so it runs whatever the image's libc is. The libc that tools mise
installs are built for is detected from the image, not from that build.
- `wget` (or `curl`), `sha256sum` and a CA bundle, for the download and the checksum
check. Any of those the image is missing are installed with `apt-get`, so an image
without them has to be Debian or Ubuntu based.

## Usage

The feature takes no options:

```json
"features": {
"ghcr.io/bare-devcontainer/features/mise:latest": {}
}
```

The feature version is the mise version it installs. `:latest` resolves to the
newest feature release when the container is built; pin the exact version to hold
a mise release:

```json
"features": {
"ghcr.io/bare-devcontainer/features/mise:2026.9.10": {}
}
```

## Installed software

- mise, the statically linked `linux-*-musl` build, installed as
`/usr/local/bin/mise` and owned by root.

Nothing else: no tool is installed until the project asks for it.

## Tool installs and download cache

mise keeps its tool installs in `~/.local/share/mise` and its download cache in
`~/.cache/mise`, its own defaults. Both are created for the remote user when the
feature is installed.

Neither directory survives a rebuild on its own; see [Tips](#tips) for keeping
them on a volume.

## Not installed

- **No language runtime or tool.** mise resolves the versions the project declares.
- **No development headers beyond libc.** A backend that downloads prebuilt binaries
works as-is; one that builds from source needs the `-dev` packages of the
libraries it links against.
- **No `PATH` entry and no shell activation.** `mise exec` and `mise run` work as
they are; see [Tips](#tips) for making tools resolve by name.

## Supply chain

The binary is downloaded from `https://github.com/jdx/mise/releases/` and checked
against `SHASUMS256.txt`, vendored with the feature: upstream's own checksum file,
committed verbatim together with the minisign signature upstream published for it.

The public key is a copy of
[`minisign.pub`](https://github.com/jdx/mise/blob/main/minisign.pub) from the mise
repository, refreshed by this repository's `Update Trusted Material` workflow.

To check the material yourself before pinning the feature:

```sh
minisign -V -m src/mise/SHASUMS256.txt \
-x src/mise/SHASUMS256.txt.minisig \
-p src/mise/mise-minisign.pub
```

`grep linux-x64 src/mise/SHASUMS256.txt` shows the release they pin.

This covers the mise binary only. Tools that mise installs at runtime are fetched
from their own upstreams under mise's own verification.

Everything downloaded during installation goes to a temporary directory that is
removed when the script exits.

## Tips

- For the tags this feature is published under, see
[Versions and pinning](https://github.com/bare-devcontainer/features#versions-and-pinning).
- To keep the installed tools and the download cache across rebuilds, add named
volumes to `mounts` in `devcontainer.json`, with the targets under the remote
user's home directory:

```json
"mounts": [
{
"source": "${devcontainerId}-mise-data",
"target": "/home/dev/.local/share/mise",
"type": "volume"
},
{
"source": "${devcontainerId}-mise-cache",
"target": "/home/dev/.cache/mise",
"type": "volume"
}
]
```
- To have tools resolve by name, put the shims directory in `remoteEnv` in
`devcontainer.json`, with the path under the remote user's home directory:

```json
"remoteEnv": {
"PATH": "/home/dev/.local/share/mise/shims:${containerEnv:PATH}"
}
```

`mise activate` in the shell configuration covers interactive shells instead.
- [`MISE_PARANOID`](https://mise.jdx.dev/paranoid.html) in `containerEnv` makes mise
re-verify the provenance of the tools it installs and refuse untrusted
configuration.
- A rebuild clears which `mise.toml` files mise has trusted. Set
`MISE_TRUSTED_CONFIG_PATHS` to the workspace folder in `containerEnv` to trust the
project's up front.
Loading
Loading