diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..11fd5ca
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Takashi Nozawa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..22fe7d8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,153 @@
+# Bare Dev Container Features
+
+[](https://github.com/bare-devcontainer/features/actions/workflows/ci.yml)
+
+[Dev Container Features](https://containers.dev/features) for Debian and Ubuntu based
+dev containers, published to `ghcr.io/bare-devcontainer/features`. Each Feature installs
+a single tool from the upstream project's own distribution channel, verified against
+signing material committed to this repository, and requires no other Feature.
+
+## Features
+
+| Feature | What you get | Source and verification |
+|---------|--------------|-------------------------|
+| [claude-code](src/claude-code)
`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. |
+| [node](src/node)
`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
+out, and how its supply chain works.
+
+## Why these features
+
+A dev container is part of the trusted development environment, and every Feature added
+to one installs software from another upstream. These Features are built so that adding
+one stays reviewable, and so that a container can be rebuilt often — which is how
+security updates arrive — without that becoming expensive:
+
+- **Minimal trusted upstreams** — software comes from the upstream project's own
+ distribution channel and nowhere else, verified with the method that upstream
+ recommends.
+- **Trusted material reviewed in this repository** — signing keys are committed here and
+ changed only through a pull request, so nothing is fetched and trusted while the
+ container is built. See [Supply chain](#supply-chain).
+- **Self-contained installs** — no other Feature is required, only the prerequisites a
+ base image turns out to be missing are installed, and nothing from the install is left
+ behind in the image.
+- **Minimal images stay minimal** — every Feature is installed and exercised on a
+ minimal Debian image as well as on Microsoft's official base image on each change, so
+ a Feature neither depends on tooling a larger image happens to carry nor drags such
+ tooling in. See [Testing](#testing).
+- **Fast, frequent rebuilds** — state a rebuild would otherwise discard, such as a
+ tool's credentials and history, is kept in a named volume declared by the Feature, so
+ rebuilding to pick up an update costs nothing but the build.
+
+## Quick start
+
+Add a Feature to `.devcontainer/devcontainer.json`, then reopen the project in a
+container:
+
+```json
+{
+ "image": "mcr.microsoft.com/devcontainers/base:debian",
+ "features": {
+ "ghcr.io/bare-devcontainer/features/node:1": {}
+ }
+}
+```
+
+Any [tool that supports dev containers](https://containers.dev/supporting) can install
+them, onto any Debian or Ubuntu based image on `linux/amd64` or `linux/arm64`.
+Requirements beyond that, such as glibc for a tool distributed as a prebuilt binary, are
+stated in the Feature's own README.
+
+Options are passed as the value of the reference. Each Feature's README lists the
+options it accepts and their defaults:
+
+```json
+"features": {
+ "ghcr.io/bare-devcontainer/features/node:1": {
+ "version": "24",
+ "keepNpm": true
+ }
+}
+```
+
+## Versions and pinning
+
+Every release publishes four tags. Using version 1.1.1 as an example:
+
+| Tag | Points at | Moves when |
+|-----|-----------|------------|
+| `1.1.1` | That exact release | Never |
+| `1.1` | The newest 1.1.x release | A patch release is published |
+| `1` | The newest 1.x release | A minor or patch release is published |
+| `latest` | The newest release | Any release |
+
+A reference is resolved when the container is built, so any tag shorter than the exact
+version resolves to different content over time. That is how `:1` picks up fixes
+automatically, and also why it is not a reproducible reference on its own — commit
+`.devcontainer/devcontainer-lock.json` to record the digest it resolved to.
+
+## Supply chain
+
+**What you are trusting.** The upstream distribution channel listed for the Feature in
+the table above, and the contents of this repository at the version you pin. Nothing
+else takes part in the install: the signing material is read from the Feature itself, so
+no key is fetched while the container is built, and no other Feature's install script
+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.
+
+**How the trusted material changes.** Only through a pull request. The
+`Update Trusted Material` workflow
+([`.github/workflows/update-material.yml`](.github/workflows/update-material.yml)) checks
+upstream weekly and opens one when a key has changed, so every rotation is visible in
+the history of this repository rather than picked up silently at build time.
+
+**What is not covered.** The upstream projects themselves are still trusted: verification
+proves a download came from them unaltered, not that what they published is sound. These
+Features are published as plain OCI artifacts, with no SLSA provenance attestation and no
+SBOM. And a floating tag still resolves to whatever is newest — see
+[Versions and pinning](#versions-and-pinning).
+
+## Testing
+
+Every Feature ships a `smoke_test.sh` next to its `devcontainer-feature.json`. On every
+change, the `CI` workflow ([`.github/workflows/ci.yml`](.github/workflows/ci.yml))
+installs each Feature at its default options on
+`mcr.microsoft.com/devcontainers/base:debian`, the official Dev Container base image
+from Microsoft, and on `ghcr.io/bare-devcontainer/debian:trixie`, a minimal Debian image
+with no `sudo`, no alternative shell and no CLI tooling beyond the basics, then runs
+that script inside the running container as the remote user.
+
+The scripts use no test helper library, so each one reads as a plain description of what
+a successful install looks like.
+
+## Related repositories
+
+- [bare-devcontainer/images](https://github.com/bare-devcontainer/images) — minimal
+ Debian based dev container images, one per language stack. When the base image can be
+ chosen freely, an image that already carries the tool is simpler than adding a
+ Feature.
+- [bare-devcontainer/templates](https://github.com/bare-devcontainer/templates) —
+ `devcontainer.json` templates for those images, with hardened defaults and cache
+ volumes. Features can be added to a template's configuration after it is applied.
+
+## Contributing
+
+Bug reports, Feature requests and pull requests are welcome. [AGENTS.md](AGENTS.md)
+describes the repository layout and the conventions a change is expected to follow.
+
+Note that `src/*/README.md` is generated by `devcontainer features generate-docs` from
+`devcontainer-feature.json` and the Feature's `NOTES.md`. Put prose in `NOTES.md`; edits
+made directly to a Feature's `README.md` are overwritten.
+
+## Security
+
+To report a vulnerability, see [SECURITY.md](SECURITY.md).
+
+## License
+
+[MIT](LICENSE)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..0ba209e
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,12 @@
+# Security Policy
+
+## Supported Versions
+
+The newest published release of each feature under `src/` is supported. Fixes are
+released as a new feature version and picked up by the floating tags (`1`, `1.1`,
+`latest`); earlier versions are not patched in place.
+
+## Reporting a Vulnerability
+
+You can report a vulnerability privately via private vulnerability reporting on GitHub.
+See [GitHub Docs](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-privately) for more information.
diff --git a/src/claude-code/NOTES.md b/src/claude-code/NOTES.md
index 558a343..507bc85 100644
--- a/src/claude-code/NOTES.md
+++ b/src/claude-code/NOTES.md
@@ -1,40 +1,51 @@
-## Details
+## Requirements
-- Requires a Debian or Ubuntu based image.
-- The repository signing key is vendored with the feature and installed to
- `/usr/share/keyrings/claude-code.asc`, so no key is downloaded at build time.
-- The APT source is written to `/etc/apt/sources.list.d/claude-code.list`. Set
- `keepAptSource` to `false` to drop the source and key once installation
- finishes.
+- A Debian or Ubuntu based image: the package is installed with `apt-get`, and the
+ install script stops early on an image without it.
-### VS Code extension
+## Usage
-The feature also requests the Claude Code VS Code extension, so an editor that
-attaches to the container gets the sidebar, inline diffs and the IDE
-integration alongside the CLI:
+The defaults install the newest package and leave the APT source in place. To drop
+the source and its key once the package is installed:
+
+```json
+"features": {
+ "ghcr.io/bare-devcontainer/features/claude-code:1": {
+ "keepAptSource": false
+ }
+}
+```
+
+`version` takes an APT package version rather than a release name, so read the exact
+string off `apt-cache policy claude-code` in a container that has the source
+configured before pinning to one.
+
+## Installed software
+
+- The `claude` CLI, from Anthropic's APT repository at
+ `https://downloads.claude.ai/claude-code/apt/stable`. The source is written to
+ `/etc/apt/sources.list.d/claude-code.list` and the signing key to
+ `/usr/share/keyrings/claude-code.asc`.
+- The Claude Code VS Code extension (`anthropic.claude-code`), installed by clients
+ that read `customizations.vscode`.
+
+To leave the extension out, list it with a minus sign in `devcontainer.json`:
```json
"customizations": {
"vscode": {
"extensions": [
- "anthropic.claude-code"
+ "-anthropic.claude-code"
]
}
}
```
-Extension installation is done by the attaching client, not by the install
-script, so this applies to VS Code, Cursor and GitHub Codespaces, and is
-ignored by clients that do not consume `customizations.vscode` (the
-`devcontainer` CLI, or attaching from another editor). The CLI is installed
-either way. Nothing in `devcontainer.json` can remove an extension a feature
-asks for, so uninstall it in the container if it is not wanted.
-
-### Persisting configuration and credentials
+## Configuration and credentials
Claude Code keeps its credentials, settings and history in its configuration
-directory. The feature declares a volume mount so those survive container
-rebuilds, and points Claude Code at it:
+directory. The feature declares a volume mount so those survive container rebuilds,
+and points Claude Code at it:
```json
"containerEnv": {
@@ -83,3 +94,33 @@ The volume is per dev container (`${devcontainerId}`) and is not shared between
projects. Mounts declared by a feature cannot be disabled from
`devcontainer.json`, but overriding `CLAUDE_CONFIG_DIR` as shown above leaves the
volume mounted and unused.
+
+## Supply chain
+
+The package is installed by APT from Anthropic's official repository, and its
+signature is checked against the repository signing key. That key is vendored with
+the feature and installed from the feature directory to
+`/usr/share/keyrings/claude-code.asc`, so nothing is trusted that was not reviewed
+in this repository — no key is downloaded at build time. It is refreshed by this
+repository's `Update Trusted Material` workflow, which opens a pull request when
+upstream publishes a different key.
+
+To see the key before pinning the feature:
+
+```sh
+gpg --show-keys src/claude-code/claude-code.asc
+```
+
+`keepAptSource` decides what is left behind. Keeping the source and key, the
+default, means `apt-get upgrade` inside the container can pick up newer releases.
+Setting it to `false` removes both once the install finishes, so the image carries
+no additional APT source. Either way the apt lists downloaded during installation
+are deleted.
+
+## Tips
+
+- For the tags this feature is published under, see
+ [Versions and pinning](https://github.com/bare-devcontainer/features#versions-and-pinning).
+- The configuration volume is per dev container, so authentication is done once per
+ project. Point `CLAUDE_CONFIG_DIR` at a mount of your own to share credentials
+ between projects.
diff --git a/src/claude-code/README.md b/src/claude-code/README.md
index 2f53e5c..a5fb3a5 100644
--- a/src/claude-code/README.md
+++ b/src/claude-code/README.md
@@ -24,43 +24,54 @@ Installs Claude Code, Anthropic's agentic coding CLI, from the official APT repo
- `anthropic.claude-code`
-## Details
+## Requirements
-- Requires a Debian or Ubuntu based image.
-- The repository signing key is vendored with the feature and installed to
- `/usr/share/keyrings/claude-code.asc`, so no key is downloaded at build time.
-- The APT source is written to `/etc/apt/sources.list.d/claude-code.list`. Set
- `keepAptSource` to `false` to drop the source and key once installation
- finishes.
+- A Debian or Ubuntu based image: the package is installed with `apt-get`, and the
+ install script stops early on an image without it.
-### VS Code extension
+## Usage
-The feature also requests the Claude Code VS Code extension, so an editor that
-attaches to the container gets the sidebar, inline diffs and the IDE
-integration alongside the CLI:
+The defaults install the newest package and leave the APT source in place. To drop
+the source and its key once the package is installed:
+
+```json
+"features": {
+ "ghcr.io/bare-devcontainer/features/claude-code:1": {
+ "keepAptSource": false
+ }
+}
+```
+
+`version` takes an APT package version rather than a release name, so read the exact
+string off `apt-cache policy claude-code` in a container that has the source
+configured before pinning to one.
+
+## Installed software
+
+- The `claude` CLI, from Anthropic's APT repository at
+ `https://downloads.claude.ai/claude-code/apt/stable`. The source is written to
+ `/etc/apt/sources.list.d/claude-code.list` and the signing key to
+ `/usr/share/keyrings/claude-code.asc`.
+- The Claude Code VS Code extension (`anthropic.claude-code`), installed by clients
+ that read `customizations.vscode`.
+
+To leave the extension out, list it with a minus sign in `devcontainer.json`:
```json
"customizations": {
"vscode": {
"extensions": [
- "anthropic.claude-code"
+ "-anthropic.claude-code"
]
}
}
```
-Extension installation is done by the attaching client, not by the install
-script, so this applies to VS Code, Cursor and GitHub Codespaces, and is
-ignored by clients that do not consume `customizations.vscode` (the
-`devcontainer` CLI, or attaching from another editor). The CLI is installed
-either way. Nothing in `devcontainer.json` can remove an extension a feature
-asks for, so uninstall it in the container if it is not wanted.
-
-### Persisting configuration and credentials
+## Configuration and credentials
Claude Code keeps its credentials, settings and history in its configuration
-directory. The feature declares a volume mount so those survive container
-rebuilds, and points Claude Code at it:
+directory. The feature declares a volume mount so those survive container rebuilds,
+and points Claude Code at it:
```json
"containerEnv": {
@@ -110,6 +121,36 @@ projects. Mounts declared by a feature cannot be disabled from
`devcontainer.json`, but overriding `CLAUDE_CONFIG_DIR` as shown above leaves the
volume mounted and unused.
+## Supply chain
+
+The package is installed by APT from Anthropic's official repository, and its
+signature is checked against the repository signing key. That key is vendored with
+the feature and installed from the feature directory to
+`/usr/share/keyrings/claude-code.asc`, so nothing is trusted that was not reviewed
+in this repository — no key is downloaded at build time. It is refreshed by this
+repository's `Update Trusted Material` workflow, which opens a pull request when
+upstream publishes a different key.
+
+To see the key before pinning the feature:
+
+```sh
+gpg --show-keys src/claude-code/claude-code.asc
+```
+
+`keepAptSource` decides what is left behind. Keeping the source and key, the
+default, means `apt-get upgrade` inside the container can pick up newer releases.
+Setting it to `false` removes both once the install finishes, so the image carries
+no additional APT source. Either way the apt lists downloaded during installation
+are deleted.
+
+## Tips
+
+- For the tags this feature is published under, see
+ [Versions and pinning](https://github.com/bare-devcontainer/features#versions-and-pinning).
+- The configuration volume is per dev container, so authentication is done once per
+ project. Point `CLAUDE_CONFIG_DIR` at a mount of your own to share credentials
+ between projects.
+
---
diff --git a/src/node/NOTES.md b/src/node/NOTES.md
index d9c2136..4763d2a 100644
--- a/src/node/NOTES.md
+++ b/src/node/NOTES.md
@@ -1,50 +1,64 @@
-## Details
+The official build from [nodejs.org](https://nodejs.org/dist/) is unpacked into
+`/usr/local`, Corepack is installed from npm, and npm itself is removed afterwards.
-This feature installs the same Node.js setup as the
-[`ghcr.io/bare-devcontainer/node`](https://github.com/bare-devcontainer/images/tree/main/node)
-image, for base images that do not already carry it: the official build from
-[nodejs.org](https://nodejs.org/dist/) unpacked into `/usr/local`, Corepack from
-npm, and no npm left behind.
+## Requirements
-- Requires a glibc based `linux-x64` or `linux-arm64` image.
-- The download, signature check and unpacking need `wget` (or `curl`), `gpgv`,
- `tar`, `xz` and `sha256sum`. Any of those the image is missing are installed
- with `apt-get`, so an image without them has to be Debian or Ubuntu based.
+- A glibc based `linux-x64` or `linux-arm64` image, since that is the official
+ build being installed.
+- `wget` (or `curl`), `gpgv`, `tar`, `xz`, `sha256sum` and a CA bundle, for the
+ download, signature check and unpacking. Any of those the image is missing are
+ installed with `apt-get`, so an image without them has to be Debian or Ubuntu
+ based; an image that already has them needs no package manager at all.
-### Version selection
+## Usage
-`version` accepts `lts` (the newest long-term support release), `latest` (the
-newest release of any line), a release line such as `24` or `24.19`, or an exact
-version such as `24.19.0`. Everything but an exact version is resolved against
-`https://nodejs.org/dist/index.json` at install time, so a rebuild picks up
-newer patch releases.
-
-### Corepack instead of npm
-
-npm and npx are removed after Corepack is installed, so the package manager
-comes from the project's `packageManager` field rather than from the image:
+The defaults install the newest long-term support release with the newest Corepack,
+and remove npm. To pin a release line and keep npm:
```json
"features": {
- "ghcr.io/bare-devcontainer/features/node:1": {}
+ "ghcr.io/bare-devcontainer/features/node:1": {
+ "version": "24",
+ "keepNpm": true
+ }
}
```
-Corepack is installed but not enabled, matching the image. Its shims go into
-`/usr/local/bin`, so enabling them needs root — a `RUN corepack enable` in a
-`Dockerfile` layered on the image, for instance. Without root, `corepack enable
---install-directory` writes them somewhere the remote user owns instead.
+## Version selection
-Set `keepNpm` to `true` to keep npm and npx, or `corepack` to `none` to leave
-Corepack out — including the copy that release lines up to Node.js 24 bundle,
-which is removed along with it. `corepack: "none"` together with `keepNpm: false`
-yields a Node.js with no package manager at all.
+`version` accepts `lts` (the newest long-term support release), `latest` (the
+newest release of any line), a release line such as `24` or `24.19`, or an exact
+version such as `24.19.0`. Everything but an exact version is resolved against
+`https://nodejs.org/dist/index.json` at install time, so a rebuild picks up
+newer patch releases.
+
+## Installed software
+
+- Node.js, unpacked into `/usr/local`, owned by root.
+- Corepack, installed globally from npm at the version `corepack` specifies.
Corepack downloads package managers into `COREPACK_HOME`, which defaults to
`~/.cache/node/corepack`. The feature creates that directory for the remote user
so the first `corepack` call does not have to write into a root-owned cache.
-### Supply chain
+## Not installed
+
+- **No npm or npx.** Both are removed after Corepack is installed, so the package
+ manager comes from the project's `packageManager` field rather than from the
+ image. Set `keepNpm` to `true` to keep them.
+- **No enabled package manager.** Corepack is installed but not enabled. Its shims go
+ into `/usr/local/bin`, so enabling them needs root — a `RUN corepack enable` in a
+ `Dockerfile` layered on the image, for instance. Without root,
+ `corepack enable --install-directory` writes them somewhere the remote user owns
+ instead.
+- **No package manager at all**, if you ask for that: `corepack: "none"` leaves
+ Corepack out — including the copy that release lines up to Node.js 24 bundle,
+ which is removed along with it — and combined with `keepNpm: false` yields a
+ Node.js with nothing to install packages with.
+- **No global JavaScript tooling.** Linters, formatters and test runners are left to
+ the project's own dependencies.
+
+## Supply chain
The release tarball is downloaded from `https://nodejs.org/dist/` and its
checksum is verified against `SHASUMS256.txt.asc`, signed by the Node.js Release
@@ -53,3 +67,28 @@ signatures are checked against keys reviewed in this repository rather than keys
fetched at install time. It is a copy of the keyring published by
[nodejs/release-keys](https://github.com/nodejs/release-keys), refreshed by this
repository's `Update Trusted Material` workflow.
+
+To see the keys before pinning the feature:
+
+```sh
+gpg --no-default-keyring --keyring "$PWD/src/node/node-keyring.kbx" --list-keys
+```
+
+Everything downloaded during installation — the tarball, the checksums and npm's
+cache — 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).
+- If you enable npm through Corepack, keep its download cache across rebuilds by
+ adding a named volume to `mounts` in `devcontainer.json`, with the target set to
+ `.npm` in the remote user's home directory:
+
+ ```json
+ {
+ "source": "${devcontainerId}-npm-cache",
+ "target": "/home/dev/.npm",
+ "type": "volume"
+ }
+ ```
diff --git a/src/node/README.md b/src/node/README.md
index cbd324c..c161a23 100644
--- a/src/node/README.md
+++ b/src/node/README.md
@@ -19,53 +19,67 @@ Installs Node.js from nodejs.org, verified against the Node.js release signing k
| corepack | npm version specifier of Corepack to install globally, or "none" to leave Corepack out. | string | latest |
| keepNpm | Keep the npm and npx executables that ship with the Node.js distribution. When false, both are removed so the package manager comes from the project's "packageManager" field through Corepack. | boolean | false |
-## Details
+The official build from [nodejs.org](https://nodejs.org/dist/) is unpacked into
+`/usr/local`, Corepack is installed from npm, and npm itself is removed afterwards.
-This feature installs the same Node.js setup as the
-[`ghcr.io/bare-devcontainer/node`](https://github.com/bare-devcontainer/images/tree/main/node)
-image, for base images that do not already carry it: the official build from
-[nodejs.org](https://nodejs.org/dist/) unpacked into `/usr/local`, Corepack from
-npm, and no npm left behind.
+## Requirements
-- Requires a glibc based `linux-x64` or `linux-arm64` image.
-- The download, signature check and unpacking need `wget` (or `curl`), `gpgv`,
- `tar`, `xz` and `sha256sum`. Any of those the image is missing are installed
- with `apt-get`, so an image without them has to be Debian or Ubuntu based.
+- A glibc based `linux-x64` or `linux-arm64` image, since that is the official
+ build being installed.
+- `wget` (or `curl`), `gpgv`, `tar`, `xz`, `sha256sum` and a CA bundle, for the
+ download, signature check and unpacking. Any of those the image is missing are
+ installed with `apt-get`, so an image without them has to be Debian or Ubuntu
+ based; an image that already has them needs no package manager at all.
-### Version selection
+## Usage
-`version` accepts `lts` (the newest long-term support release), `latest` (the
-newest release of any line), a release line such as `24` or `24.19`, or an exact
-version such as `24.19.0`. Everything but an exact version is resolved against
-`https://nodejs.org/dist/index.json` at install time, so a rebuild picks up
-newer patch releases.
-
-### Corepack instead of npm
-
-npm and npx are removed after Corepack is installed, so the package manager
-comes from the project's `packageManager` field rather than from the image:
+The defaults install the newest long-term support release with the newest Corepack,
+and remove npm. To pin a release line and keep npm:
```json
"features": {
- "ghcr.io/bare-devcontainer/features/node:1": {}
+ "ghcr.io/bare-devcontainer/features/node:1": {
+ "version": "24",
+ "keepNpm": true
+ }
}
```
-Corepack is installed but not enabled, matching the image. Its shims go into
-`/usr/local/bin`, so enabling them needs root — a `RUN corepack enable` in a
-`Dockerfile` layered on the image, for instance. Without root, `corepack enable
---install-directory` writes them somewhere the remote user owns instead.
+## Version selection
+
+`version` accepts `lts` (the newest long-term support release), `latest` (the
+newest release of any line), a release line such as `24` or `24.19`, or an exact
+version such as `24.19.0`. Everything but an exact version is resolved against
+`https://nodejs.org/dist/index.json` at install time, so a rebuild picks up
+newer patch releases.
+
+## Installed software
-Set `keepNpm` to `true` to keep npm and npx, or `corepack` to `none` to leave
-Corepack out — including the copy that release lines up to Node.js 24 bundle,
-which is removed along with it. `corepack: "none"` together with `keepNpm: false`
-yields a Node.js with no package manager at all.
+- Node.js, unpacked into `/usr/local`, owned by root.
+- Corepack, installed globally from npm at the version `corepack` specifies.
Corepack downloads package managers into `COREPACK_HOME`, which defaults to
`~/.cache/node/corepack`. The feature creates that directory for the remote user
so the first `corepack` call does not have to write into a root-owned cache.
-### Supply chain
+## Not installed
+
+- **No npm or npx.** Both are removed after Corepack is installed, so the package
+ manager comes from the project's `packageManager` field rather than from the
+ image. Set `keepNpm` to `true` to keep them.
+- **No enabled package manager.** Corepack is installed but not enabled. Its shims go
+ into `/usr/local/bin`, so enabling them needs root — a `RUN corepack enable` in a
+ `Dockerfile` layered on the image, for instance. Without root,
+ `corepack enable --install-directory` writes them somewhere the remote user owns
+ instead.
+- **No package manager at all**, if you ask for that: `corepack: "none"` leaves
+ Corepack out — including the copy that release lines up to Node.js 24 bundle,
+ which is removed along with it — and combined with `keepNpm: false` yields a
+ Node.js with nothing to install packages with.
+- **No global JavaScript tooling.** Linters, formatters and test runners are left to
+ the project's own dependencies.
+
+## Supply chain
The release tarball is downloaded from `https://nodejs.org/dist/` and its
checksum is verified against `SHASUMS256.txt.asc`, signed by the Node.js Release
@@ -75,6 +89,31 @@ fetched at install time. It is a copy of the keyring published by
[nodejs/release-keys](https://github.com/nodejs/release-keys), refreshed by this
repository's `Update Trusted Material` workflow.
+To see the keys before pinning the feature:
+
+```sh
+gpg --no-default-keyring --keyring "$PWD/src/node/node-keyring.kbx" --list-keys
+```
+
+Everything downloaded during installation — the tarball, the checksums and npm's
+cache — 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).
+- If you enable npm through Corepack, keep its download cache across rebuilds by
+ adding a named volume to `mounts` in `devcontainer.json`, with the target set to
+ `.npm` in the remote user's home directory:
+
+ ```json
+ {
+ "source": "${devcontainerId}-npm-cache",
+ "target": "/home/dev/.npm",
+ "type": "volume"
+ }
+ ```
+
---