OpenShell is NVIDIA's runtime environment for autonomous AI agents. It provides isolated sandboxes where agents can safely run and iterate — without risk to the host system or your credentials.
OpenShell ships a set of pre-built sandbox images, but they are general-purpose. openshell-image-builder lets you build your own: lightweight, workspace-specific images that contain only what you need — without writing a Containerfile by hand.
The tool assembles the image in layers — base image, agent installation, agent settings, OpenShell network policy, and project-specific toolchains:
- Base image — Ubuntu, Fedora, Red Hat UBI, or Red Hat Hardened Images (HummingBird), any tag. Ubuntu 24.04 is the default.
- Agent installation (
--agent) — the agent binary is pre-installed inPATH. - Agent settings
- User settings — settings files are pre-populated with settings files provided by the user.
- Auto-onboarding — settings files are updated to skip onboarding steps (choose theme, trust directory, etc).
- Skills — skills are copied in the agent's skills directory.
- Inference settings (
--inference) — inference provider definition is added to settings files. - Endpoint override (
--endpoint) — optional custom URL for inference provider is set in inference provider definition. - Model (
--model) — default model is baked into the agent's settings files.
- OpenShell policy —
/etc/openshell/policy.yamlshipped with every image.- Base policy — Git operations over HTTPS and the GitHub REST API.
- Agent network rules — agent-specific endpoints are added by
--agent. - Inference network rules — LLM backend endpoints are added by
--inference.
- Installation of project-specific toolchains — toolchains and utilities declared as Dev Container Features in
.kaiden/workspace.jsonare installed in the image.
| Agent | User settings | Auto-onboarding | Skills |
|---|---|---|---|
claude |
Yes | Yes | Yes~/.claude/skills/ |
opencode |
Yes | N/A | Yes~/.opencode/skills/ |
| Agent | Inference | Inference settings | Endpoint override | Model selection |
|---|---|---|---|---|
claude |
anthropic |
N/A | YesENV ANTHROPIC_BASE_URL |
Yesmodel in .claude/settings.json |
claude |
vertexai |
N/A | No fixed endpoint |
Yesmodel in .claude/settings.json |
opencode |
anthropic |
N/A, Yes if endpoint override | Yes opencode config baseURL |
Yesmodel in .config/opencode/config.json |
opencode |
vertexai |
N/A | No fixed endpoint |
Yesmodel in .config/opencode/config.json |
opencode |
ollama |
Yes Ollama provider config |
YesbaseURL in Ollama provider config |
Yesmodel in .config/opencode/config.json |
opencode |
openai |
Yes if model or endpoint | YesbaseURL in custom provider config |
Yesmodel in .config/opencode/config.json |
Build an image with a single command:
openshell-image-builder myimage:latest<TAG> is the only required argument — it sets the tag for the built image. By default, the tool uses Ubuntu 24.04 as the base image.
To use a different base image or tag, create a configuration file.
The tool looks for a config.toml file in this order, using the first directory found:
- Directory given by the
--configflag - Directory set in the
OPENSHELL_IMAGE_BUILDER_CONFIGenvironment variable - The platform config directory:
- Linux:
$XDG_CONFIG_HOME/openshell-image-builder/(defaults to~/.config/openshell-image-builder/) - macOS:
~/Library/Application Support/openshell-image-builder/ - Windows:
%APPDATA%\openshell-image-builder\
- Linux:
If no config.toml is found in the resolved directory, or the file is empty, built-in defaults are used.
If a directory is given explicitly (via --config or the environment variable) but it does not exist, the command fails immediately.
Ubuntu (default)
[openshell_image_builder.base_image]
image = "ubuntu"
tag = "24.04"Fedora
[openshell_image_builder.base_image]
image = "fedora"
tag = "latest"Red Hat UBI
[openshell_image_builder.base_image]
image = "ubi"
tag = "latest"Red Hat Hardened Images (Hummingbird)
[openshell_image_builder.base_image]
image = "hummingbird"
tag = "latest-builder"[openshell_image_builder]
version = 1
[openshell_image_builder.base_image]
image = "ubuntu" # "ubuntu", "fedora", "ubi", or "hummingbird"
tag = "24.04"| Field | Default | Description |
|---|---|---|
openshell_image_builder.version |
1 |
Configuration schema version |
openshell_image_builder.base_image.image |
ubuntu |
Base image name (ubuntu, fedora, ubi, or hummingbird) |
openshell_image_builder.base_image.tag |
24.04 |
Base image tag — Ubuntu: 24.04, 22.04, …; Fedora: latest, 43, 42, …; UBI: latest, 10.2-1780377767, …; Hummingbird: latest-builder, … |
Pass --config to point to a directory explicitly (the tool reads config.toml inside it):
openshell-image-builder --config /path/to/config/dir myimage:latestOr set the environment variable instead:
OPENSHELL_IMAGE_BUILDER_CONFIG=/path/to/config/dir openshell-image-builder myimage:latestUse -v (info) or -vv (debug) to increase log verbosity — useful for tracing which config file is loaded:
openshell-image-builder -v myimage:latestPass --agent to install an agent into the image.
| Agent | Value | Description |
|---|---|---|
| Claude Code | claude |
Anthropic's Claude Code CLI |
| OpenCode | opencode |
OpenCode AI coding agent |
openshell-image-builder --agent claude myimage:latest
openshell-image-builder --agent opencode myimage:latestYou can pre-populate the sandbox home directory with settings files specific to an agent. Place the files under:
<settings dir>/agents/<agent>/
where <settings dir> is the directory described in Configuring the base image, and <agent> matches the value passed to --agent (claude or opencode).
All files and subdirectories are copied into /sandbox/ (the sandbox user's home directory), owned by the sandbox user. The copy happens before the agent is installed, so the agent installer can create additional files on top without overwriting your settings.
mkdir -p ~/.config/openshell-image-builder/agents/claude/.claude
cp ~/.claude/settings.json \
~/.config/openshell-image-builder/agents/claude/.claude/settings.json
openshell-image-builder --agent claude myimage:latestThe file will be present at /sandbox/.claude/settings.json in the image.
mkdir -p ~/.config/openshell-image-builder/agents/opencode/.config/opencode
cp ~/.config/opencode/config.json \
~/.config/openshell-image-builder/agents/opencode/.config/opencode/config.json
openshell-image-builder --agent opencode myimage:latestThe file will be present at /sandbox/.config/opencode/config.json in the image.
When --agent claude is used, the builder automatically creates or updates /sandbox/.claude.json with the following settings to skip the interactive onboarding dialogs that would otherwise appear on first launch:
hasCompletedOnboarding: true— marks the setup wizard as complete.projects["/sandbox"].hasTrustDialogAccepted: true— pre-accepts the workspace trust prompt for the sandbox home directory.
If you provide your own .claude.json in the agent settings directory, the builder merges these fields into it, preserving any other fields you have set.
Pass --inference to allow the agent to reach its LLM backend. This is separate from --agent because the same inference provider can serve multiple agents.
| Inference | Value | Agents | Description |
|---|---|---|---|
| Anthropic | anthropic |
claude, opencode |
Anthropic API (api.anthropic.com) |
| Vertex AI | vertexai |
claude, opencode |
Google Vertex AI (oauth2.googleapis.com, aiplatform.googleapis.com, *-aiplatform.googleapis.com) |
| Ollama | ollama |
opencode |
Local models on the host machine, reached via host.openshell.internal:11434 |
| OpenAI | openai |
opencode |
OpenAI API (api.openai.com), or any OpenAI-compatible endpoint via --endpoint |
openshell-image-builder --agent claude --inference anthropic myimage:latest
openshell-image-builder --agent opencode --inference anthropic myimage:latest
openshell-image-builder --agent claude --inference vertexai myimage:latest
openshell-image-builder --agent opencode --inference vertexai myimage:latest
openshell-image-builder --agent opencode --inference ollama myimage:latest
openshell-image-builder --agent opencode --inference openai myimage:latestUse --endpoint to override the inference provider's default URL — useful for routing through a proxy, a local instance, or a non-default port.
| Agent | Inference | Supported | Effect |
|---|---|---|---|
claude |
anthropic |
✅ | Baked into the image as ENV ANTHROPIC_BASE_URL=<url> |
claude |
vertexai |
❌ | Rejected — Vertex AI has a proprietary fixed endpoint |
opencode |
anthropic |
✅ | Written to opencode config as provider.anthropic.options.baseURL |
opencode |
vertexai |
❌ | Rejected — Vertex AI has a proprietary fixed endpoint |
opencode |
ollama |
✅ | Written to opencode config as provider.ollama.options.baseURL; localhost in the URL is rewritten to host.openshell.internal; defaults to http://host.openshell.internal:11434/v1 if omitted |
opencode |
openai |
✅ | When provided, opencode is configured to use a custom @ai-sdk/openai-compatible provider with options.baseURL set to the given URL |
# Route Claude Code through a custom Anthropic API proxy
openshell-image-builder \
--agent claude --inference anthropic \
--endpoint https://my-anthropic-proxy.example.com \
myimage:latest
# Route OpenCode through a custom Anthropic API proxy
openshell-image-builder \
--agent opencode --inference anthropic \
--endpoint https://my-anthropic-proxy.example.com \
myimage:latest
# Connect OpenCode to Ollama running on a non-default port
openshell-image-builder \
--agent opencode --inference ollama \
--endpoint http://localhost:9999/v1 \
myimage:latestUse --model to bake a default model into the image. The agent uses this model without requiring a runtime flag.
| Agent | Inference | Effect |
|---|---|---|
claude |
any | Written to .claude/settings.json as "model": "<model>" |
opencode |
anthropic |
Written to opencode config as top-level "model" field (can be combined with --endpoint) |
opencode |
vertexai |
Written to opencode config as top-level "model" field |
opencode |
ollama |
Written to opencode config as top-level "model": "ollama/<model>" field; only the specified model is registered in the models map |
opencode |
openai |
Written to opencode config as "model": "openai/<model>" (native OpenAI) or "model": "custom/<model>" (with --endpoint) |
# Pin Claude Code to a specific model
openshell-image-builder \
--agent claude --inference anthropic \
--model claude-opus-4-8 \
myimage:latest
# Pin OpenCode to a specific Anthropic model
openshell-image-builder \
--agent opencode --inference anthropic \
--model claude-opus-4-8 \
myimage:latest
# Pin OpenCode to a specific Ollama model
openshell-image-builder \
--agent opencode --inference ollama \
--model qwen3-coder:30b \
myimage:latestThe model string is passed through as-is — use whatever identifier your agent and provider expect.
When --agent opencode --inference ollama is used, the builder automatically writes /sandbox/.config/opencode/config.json to configure OpenCode's Ollama provider:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://host.openshell.internal:11434/v1"
},
"models": {
"lfm2.5": { "tools": true },
"qwen3-coder:30b": { "tools": true }
}
}
}
}When --model is also given, the top-level "model" field is added (as "ollama/<model>") and the models map is replaced with a single entry for the specified model.
host.openshell.internal is the hostname used inside the sandbox to reach the container host, where Ollama is expected to be running on its default port (11434). Ollama must be running on the host before the sandbox is started.
Every image built by this tool includes /etc/openshell/policy.yaml. This file is read by the OpenShell runtime and defines the sandbox security policy for the container:
- Filesystem policy — which paths are read-only, read-write, or inaccessible to the
sandboxuser. - Network policies — which binaries are allowed to connect to which hosts and ports.
The policy is built in three layers, merged in order:
- Base (
assets/policy.yaml) — general-purpose tooling: Git operations over HTTPS and the GitHub REST API viagh. - Inference (added by
--inference) — LLM backend endpoints scoped to the agent binary. For example,--inference anthropicaddsapi.anthropic.comandstatsig.anthropic.com;--inference vertexaiaddsoauth2.googleapis.comandaiplatform.googleapis.com(including the*-aiplatform.googleapis.comwildcard);--inference ollamaaddshost.openshell.internal:11434for local model access;--inference openaiaddsapi.openai.com(or the custom endpoint host when--endpointis used). - Agent (added by
--agent) — agent-specific endpoints. For example,--agent claudeaddsplatform.claude.com,raw.githubusercontent.com, and the GitHub REST API for Claude's coding tools;--agent opencodeaddsopencode.ai,registry.npmjs.org, andmodels.dev.
The tool supports Dev Container Features declared in .kaiden/workspace.json in the current directory.
{
"features": {
"<feature-ref>": {
"<option>": "<value>"
}
}
}Each key in features is a feature reference; each value is a map of options passed to the feature's install.sh.
| Format | Example | Resolves to |
|---|---|---|
| OCI registry reference | ghcr.io/devcontainers/features/rust:1 |
downloaded from registry |
| Local path | ./my-feature |
.kaiden/my-feature/ |
Local paths are resolved relative to .kaiden/: ./my-feature points to .kaiden/my-feature/.
OCI references without an explicit registry default to ghcr.io. Tags and digests (@sha256:…) are both supported. Direct http:// / https:// tarball URLs are not supported.
{
"features": {
"ghcr.io/devcontainers/features/rust:1": {
"version": "stable",
"profile": "minimal"
},
"./my-feature": {}
}
}With the above, ./my-feature refers to a local feature at .kaiden/my-feature/.
Features are installed in the order defined by each feature's installsAfter field in its devcontainer-feature.json. Within the same dependency level, features are processed in alphabetical order by reference.
Agents can be extended with skills — named toolkits that an agent discovers at startup. Declare skill directories in .kaiden/workspace.json:
{
"skills": [
"./my-skill",
"./.kaiden/skills/another-skill"
]
}Each entry is a path to a directory (relative to the workspace root). The directory name becomes the skill name in the image.
During the build, each skill directory is copied into the agent's skills directory:
| Agent | Skills directory |
|---|---|
claude |
/sandbox/.claude/skills/ |
opencode |
/sandbox/.opencode/skills/ |
With --agent claude and "skills": ["./my-skill"], the skill lands at /sandbox/.claude/skills/my-skill/ in the image, owned by the sandbox user.
Skills without a corresponding --agent flag are silently ignored — the agent determines where skills go, so a build without an agent produces no skill COPY instructions.
When .kaiden/workspace.json is present, the tool:
- Downloads and extracts each OCI feature into a temporary build context directory (
/tmp/openshell-image-builder…). - Copies local feature directories into the same build context.
- Passes the build context to
podman build, where each feature is installed via:COPY features/<dir>/ /tmp/feature-install/<dir>/ RUN chmod +x /tmp/feature-install/<dir>/install.sh && \ OPTION="value" /tmp/feature-install/<dir>/install.sh
- Cleans up all feature files from the image with
RUN rm -rf /tmp/feature-installafter all features are installed.
Features run as root so install scripts can write to system paths.
openshell-image-builder [OPTIONS] <TAG>
| Argument / Option | Description |
|---|---|
<TAG> |
Tag for the built image (e.g. myimage:latest) |
--config <CONFIG> |
Path to config directory containing config.toml (env: OPENSHELL_IMAGE_BUILDER_CONFIG) |
--agent <AGENT> |
Agent to install in the image (claude, opencode) |
--inference <INFERENCE> |
Inference server the agent will connect to (anthropic, vertexai, ollama, openai) |
--endpoint <URL> |
Override the inference provider's default endpoint URL (see Custom endpoint) |
--model <MODEL> |
Default model for the agent to use (see Default model) |
-v / -vv |
Increase log verbosity (info / debug) |
$ openshell-image-builder \
--agent claude \
--inference anthropic \
--model claude-sonnet-4-6 \
sandbox_image:claude_anthropic
$ openshell provider create \
--type generic \
--credential ANTHROPIC_API_KEY=sk-ant-... \
--name claude_anthropic_provider
$ openshell sandbox create \
--from sandbox_image:claude_anthropic \
--provider claude_anthropic_provider \
--upload . \
--name claude_anthropic_sandbox \
--no-auto-providers \
-- claude
$ openshell-image-builder \
--agent opencode \
--inference anthropic \
--model claude-sonnet-4-6 \
sandbox_image:opencode_anthropic
$ openshell provider create \
--type generic \
--credential ANTHROPIC_API_KEY=sk-ant-... \
--name opencode_anthropic_provider
$ openshell sandbox create \
--from sandbox_image:opencode_anthropic \
--provider opencode_anthropic_provider \
--upload . \
--name opencode_anthropic_sandbox \
--no-auto-providers \
-- opencode
$ openshell-image-builder \
--agent claude \
--inference vertexai \
--model claude-sonnet-4-6 \
sandbox_image:claude_vertexai
# change value of ANTHROPIC_VERTEX_PROJECT_ID and CLOUD_ML_REGION
$ openshell sandbox create \
--from sandbox_image:claude_vertexai \
--upload . \
--name claude_vertexai_sandbox \
--no-auto-providers \
--no-tty \
-- bash -c '(\
echo export CLAUDE_CODE_USE_VERTEX=1; \
echo export ANTHROPIC_VERTEX_PROJECT_ID=my-gcp-project; \
echo export CLOUD_ML_REGION=global \
) >> /sandbox/.bashrc'
$ openshell sandbox upload \
claude_vertexai_sandbox \
$HOME/.config/gcloud/application_default_credentials.json \
/sandbox/.config/gcloud/application_default_credentials.json
$ openshell sandbox connect claude_vertexai_sandbox
sandbox:~$ claude
Ollama must be running on the host before starting the sandbox.
$ openshell-image-builder \
--agent opencode \
--inference ollama \
--model qwen3-coder:30b \
sandbox_image:opencode_ollama
$ openshell sandbox create \
--from sandbox_image:opencode_ollama \
--upload . \
--name opencode_ollama_sandbox \
--no-auto-providers \
-- opencode
$ openshell-image-builder \
--agent opencode \
--inference openai \
--model gpt-4o \
sandbox_image:opencode_openai
$ openshell provider create \
--type generic \
--credential OPENAI_API_KEY=sk-... \
--name opencode_openai_provider
$ openshell sandbox create \
--from sandbox_image:opencode_openai \
--provider opencode_openai_provider \
--upload . \
--name opencode_openai_sandbox \
--no-auto-providers \
-- opencodeTo use an OpenAI-compatible endpoint (e.g. Azure OpenAI, a local proxy, or another provider's API):
$ openshell-image-builder \
--agent opencode \
--inference openai \
--endpoint https://my-openai-proxy.example.com/v1 \
--model gpt-4o \
sandbox_image:opencode_openai_custom$ openshell-image-builder \
--agent opencode \
--inference vertexai \
--model claude-sonnet-4-6 \
sandbox_image:opencode_vertexai
# change value of GOOGLE_CLOUD_PROJECT and VERTEX_LOCATION
$ openshell sandbox create \
--from sandbox_image:opencode_vertexai \
--upload . \
--name opencode_vertexai_sandbox \
--no-auto-providers \
--no-tty \
-- bash -c '(\
echo export GOOGLE_CLOUD_PROJECT=my-gcp-project; \
echo export VERTEX_LOCATION=global; \
echo export GOOGLE_APPLICATION_CREDENTIALS=/sandbox/.config/gcloud/application_default_credentials.json \
) >> /sandbox/.bashrc'
$ openshell sandbox upload \
opencode_vertexai_sandbox \
$HOME/.config/gcloud/application_default_credentials.json \
/sandbox/.config/gcloud/application_default_credentials.json
$ openshell sandbox connect opencode_vertexai_sandbox
sandbox:~$ opencode
# select a model with /models