Skip to content

Repository files navigation

h-network/base — a general-purpose dev container for the agent CLIs

One image, every machineRegistry: ghcr.ioPlatforms: amd64 + arm64License: Apache 2.0

buildUbuntu 24.04Docker: multi-archAgents: claude codex agytmux: configuredImage: ~480 MB

A general-purpose development container: the Claude, Codex and Antigravity CLIs
plus everyday dev tooling, on Ubuntu 24.04.

Built for sandbox work — spin one up on any machine with Docker and get the same environment every time, without reinstalling toolchains by hand.

Quick start · What's inside · Helpers · Details · Layout


🚀 Quick start

Two ways to get it. Both give the same image.

Pull the published imageBuild it yourself
docker run -it --rm \
ghcr.io/h-network/base:latest

No clone, no build.

git clone https://github.com/h-network/base.git
cd base
docker compose build
docker compose run --rm dev

Published for linux/amd64 and linux/arm64, so Docker pulls whichever matches the machine. :latest tracks main; version tags (:1, :1.2, :1.2.3) are published from git tags and are what you want to depend on — they do not move.

Building always fetches the current release of each agent CLI, so a rebuild is how you refresh them. A published tag is frozen at whenever it was built.

📦 What's inside

Agent CLIsclaude, codex, agy
HelpersstartAgent, setupConfigDir, seedProfile, probeProvider, smokeTest
Devgit, gh, python3, python3-venv, python3-pip, build-essential
Shelltmux (configured), vim-tiny, openssh-client, sudo
Baseubuntu:24.04, UTF-8 locale

Roughly 480 MB.

🧰 Helpers

startAgent — launch a CLI with the container's defaults

startAgent # $AGENT_CLI, default claude
startAgent codex
startAgent claude --resume # extra arguments pass through

Each CLI spells "don't stop to ask me" differently — --dangerously-skip-permissions for claude and agy, --dangerously-bypass-approvals-and-sandbox for codex. startAgent knows which is which so you don't have to.

Warning

Approval prompts are skipped by default, because the flags above are meant for exactly this situation: a disposable container running as an unprivileged user. That assumption breaks the moment you bind-mount a host directory you care about — set AGENT_SKIP_PERMISSIONS=0 to keep the prompts.

variable
AGENT_CLIdefault CLI when none is named (default claude)
AGENT_SKIP_PERMISSIONS1 (default) skips approval prompts, 0 keeps them
AGENT_CLAUDE_TOOLSclaude's tool list (default Bash Read Write Edit Glob Grep; empty = unrestricted)

Only claude can restrict its tool set from the command line — agy and codex have no equivalent, so they run with their full set.

Pointing claude at a local inference endpoint

Supported for claude only. Describe the intent and startAgent translates it into the several ANTHROPIC_* variables claude actually reads:

AGENT_PROVIDER_URL=http://10.0.0.5:8000 \
AGENT_PROVIDER_MODEL=some-model-id \
startAgent claude
variable
AGENT_PROVIDER_URLendpoint base; a trailing /v1 is stripped here, because claude appends /v1/messages itself
AGENT_PROVIDER_MODELmodel id, byte for byte as the endpoint serves it
AGENT_PROVIDER_SMALL_MODELoptional, defaults to AGENT_PROVIDER_MODEL
AGENT_PROVIDER_TOKENoptional; a placeholder is sent, since claude will not start without one

All three model tiers are set from these, because claude picks a tier internally — setting only some leaves the rest pointing at vendor model names the endpoint does not serve, which claude reports as a model error. Any inherited ANTHROPIC_* variables are cleared first, so a leftover key from a previous subscription cannot quietly win.

Warning

codex and agy refuse to start when AGENT_PROVIDER_URL is set, and exit non-zero. Neither can be pointed at a local endpoint, and starting them anyway would run the agent against the vendor — different cost, different destination for your code, and nothing on screen to say so. Setting that variable states an intent, and failing it is an error rather than a fallback.

A successful run against a local endpoint still prints this once, on stderr:

[claude-code:unrecognized_model] {"model":"…","query_source":"generate_session_title"}

It is claude noting that the model is not one of its own while naming the session, not a failure — the run continues and answers normally.

setupConfigDir — a second config dir, seeded from your profile

setupConfigDir work # → ~/.claude-work + ~/.codex-work
setupConfigDir work --same # ...and copy the existing logins across
setupConfigDir # show what this shell is using
setupConfigDir default # back to ~/.claude and ~/.codex

Exporting CLAUDE_CONFIG_DIR by hand points the CLI at an empty directory, so it quietly runs on stock defaults instead of the profile you set up. setupConfigDir copies your profile in first — settings.json, config.toml, and any skills/, agents/ or CLAUDE.md beside them. Session history and logins are left behind, which is what makes the new dir separate; --same copies the logins too.

It sets CLAUDE_CONFIG_DIR and CODEX_HOME together, and applies to the calling shell only — a new shell is back on the defaults.

seedProfile — a config dir for one CLI, for callers that are not a shell

seedProfile claude work # → CLAUDE_CONFIG_DIR=~/.claude-work
seedProfile codex work # → CODEX_HOME=~/.codex-workeval"$(seedProfile --export claude work)"

A CLI pointed at a fresh directory does not fall back to the defaults in ~/.claude — measured, not assumed: CLAUDE_CONFIG_DIR=~/.claude-test claude stops on the theme picker. An automated caller that creates a directory and starts an agent against it therefore gets a process that waits forever for a keypress, and looks exactly like an agent with nothing to say.

seedProfile copies the defaults out of the live config dir at runtime, so there is one source of truth rather than a second copy that drifts. Existing files are never overwritten, and credentials are not copied — a seeded profile is unauthenticated by design.

It differs from setupConfigDir in who it is for: that one is a shell function that exports into your shell and moves both CLIs together; this one takes a single CLI, changes no environment, and prints the variable to set.

agy is not supported and says so — it resolves its state relative to HOME under ~/.gemini/antigravity-cli/, with no variable to point it elsewhere.

probeProvider — see what a local endpoint serves, and whether claude can use it

probeProvider http://10.0.0.5:8000 # list, then verify with the first id
probeProvider http://10.0.0.5:8000 some-model # verify with a specific one

Lists model ids from /v1/models (vLLM and anything else OpenAI-shaped), falling back to /api/tags for ollama, then POSTs to /v1/messages to check claude can actually talk to it — serving models and implementing the route claude uses are not the same thing.

The ids are printed rather than described because they have to match exactly; an ollama tag like gpt-oss:20b mistyped as gpt-oss-20b comes back later as a model error rather than as a typo.

It waits up to 90 seconds (PROBE_TIMEOUT) and reports "no answer" separately from "not usable": a model loading cold can take tens of seconds, and treating that as a verdict on the endpoint sends you to debug the wrong thing.

smokeTest — check the entry points still exist

smokeTest # exits 0, or lists what is broken and exits 1

Runs during the build and fails it, so an image that publishes has at least been shown to have a working startAgent, all three CLIs on PATH, and config files that parse. Re-runnable inside a container that is already up.

It proves only that those things exist and start — no model, no credentials, no network — and it says so in its own output, because a green build here is easy to read as more assurance than it is.

🔍 Details worth knowing

Runs as ubuntu (uid 1000), not root

The agent CLIs misbehave under root — Claude Code refuses --dangerously-skip-permissions there. sudo is available without a password.

Nothing is bind-mounted by default

compose.yaml uses named volumes, so agent logins and /workspace survive --rm without inheriting host file ownership. Work is expected to live in git, cloned inside the container.

If you do bind-mount a host directory, note that the image is uid 1000 — on a host where you are not uid 1000, writes will fail. Either chown -R 1000:1000 the directory or run with --user $(id -u):$(id -g).

Python packages need a venv

Ubuntu 24.04 is PEP 668 managed, so system-wide pip install is refused by design:

python3 -m venv .venv && .venv/bin/pip install <package>
The first-run dialogs are already answered

Each CLI opens on a dialog the first time it runs and waits for a keypress. With nobody at the terminal that is indistinguishable from an idle agent — no error, no exit code, no log line — so the image answers them in advance:

dialogkeyfile
theme pickerhasCompletedOnboarding~/.claude.json
bypass-permissions acceptanceskipDangerousModePermissionPrompt~/.claude/settings.json
"Update available!"check_for_update_on_startup~/.codex/config.toml
"Do you trust this folder?" for /workspacehasTrustDialogAccepted~/.claude.json

~/.claude/settings.json also turns off telemetry and error reporting, blanks the attribution fields so nothing is added to commit messages or PR bodies, and denies mcp__* so a cloned repository cannot introduce tool surface you did not approve. ~/.codex/config.toml keeps approval_policy and sandbox_mode set even though startAgent passes equivalents — a codex run directly, without the wrapper, would otherwise stall.

None of this is a login; the CLIs still ask you to authenticate.

[!NOTE] A consumer that ships its own settings.jsonreplaces this file rather than merging with it, and so stops inheriting anything added here later — including suppression for a dialog that does not exist yet. That is the intended escape hatch, but it is worth choosing deliberately.

The trust prompt is keyed by absolute path, so only /workspace — the image's WORKDIR, and where an agent starts unless told otherwise — is answered ahead of time. Working anywhere else prompts once on first use.

No credentials are baked into the image

Authenticate inside the container (claude, codex, gh auth login); the compose volumes persist it.

📁 Layout

Dockerfile the image
LICENSE Apache 2.0
NOTICE what the licence covers, and what it does not
compose.yaml build + run, named volumes
tmux.conf copied to ~/.tmux.conf
claude-settings.json → ~/.claude/settings.json
codex-config.toml → ~/.codex/config.toml
startAgent.sh → /usr/local/bin/startAgent
setupconfigdir.sh → /etc/profile.d/ (a shell function, so it can export)
smoketest.sh → /usr/local/bin/smokeTest, and run during the build
probeprovider.sh → /usr/local/bin/probeProvider
seedprofile.sh → /usr/local/bin/seedProfile
.github/workflows/build.yml builds and publishes to GHCR
docs/assets/banner.svg the banner above
docs/assets/badges/ the badges above — self-hosted, not shields.io, so
the README fetches nothing outside github.com
docs/assets/mkbadges.py regenerates docs/assets/badges/

🚢 Cutting a release

Push a version tag; the workflow builds both architectures and publishes them.

git tag v1.2.3 && git push origin v1.2.3

main publishes :main and :latest on every push. There are no tarball releases — the image is pulled from GHCR.

📄 License

Apache License 2.0. Every file in this repository carries an SPDX-License-Identifier: Apache-2.0 header, and the image declares org.opencontainers.image.licenses=Apache-2.0.

That covers this repository's own files — the Dockerfile, scripts and config. It does not cover what the build installs into the image: Ubuntu's packages, gh, and the three agent CLIs all keep their own terms. NOTICE has the details, and both files ship inside the image at /usr/share/doc/h-network-base/.

Note

The agent CLIs are not open-source components. Publishing an image built from this repository redistributes them, which the Apache grant here says nothing about — check each vendor's terms first.

About

A general-purpose development container — agent CLIs plus everyday tooling on Ubuntu 24.04, running unprivileged. Built and published to GHCR for amd64 and arm64

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages