The kilo-code container¶
kilo-code packages Kilo CLI, the open-source,
provider-neutral terminal coding agent, in the repository's standard hardened
workload environment: non-root execution, a read-only root filesystem, minimal
Linux capabilities, and deny-by-default outbound networking.
It uses python:3.14-slim-trixie plus Node LTS. Kilo CLI is installed from
the official @kilocode/cli npm package. Python, uv, and uvx are included
so Kilo can run Python-based MCP servers and project tooling without a custom
derivative image.
Build¶
Matching your host user's UID/GID¶
The image defaults to UID/GID 1000:1000. Everything the agent writes to
the bind-mounted project directory or the persistent home volume is owned
by whatever UID/GID the container ran as — if that doesn't match your host
account, those files aren't owned by you on the host (permission errors,
needing sudo to clean up). Rebuild with matching values instead:
docker build --build-context shared=agent-images/shared \
--build-arg UID=$(id -u) --build-arg GID=$(id -g) \
-t kilo-code:$(whoami) agent-images/kilo-code
Dockerfile splits into a base stage that installs everything —
apt/Node.js/uv baseline, packages-apt.txt/packages-uv.txt, Kilo CLI
itself, and the account-scoped packages-npm.txt/tools-uv.txt — under a
fixed placeholder account (UID/GID 1000:1000), and a final stage that
only remaps that account to the UID/GID build args (usermod/groupmod
plus a chown -R of what base already installed) when they differ from
the placeholder. Nothing in final reinstalls packages, so rebuilding for a
different UID after the first build is fast.
On a host shared by multiple accounts, tag the image per user
(kilo-code:alice, kilo-code:bob) rather than overwriting a shared
kilo-code tag — and use that same tag, plus a per-user home volume
(kilo-home-alice rather than the shared kilo-home), in the run command
below. The shell-shortcut
functions do both
substitutions automatically from a single CONTAINED_KILO_TAG variable; if
invoking docker run directly instead, substitute your tag into both the
kilo-home volume name and the trailing kilo-code image reference in the
command below — copying it unchanged still runs the shared
kilo-code:latest.
Run¶
This example permits only Kilo's hosted AI Gateway. It does not implicitly
permit any direct model provider, MCP server, package registry, or source host.
Configure a Kilo account or provider from Kilo's /connect flow, then add only
the hosts that configuration needs.
docker run -it --rm \
--security-opt=no-new-privileges \
--read-only --tmpfs /tmp:exec --tmpfs /run \
--cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID \
-e 'AGENT_ALLOWED_EGRESS=api.kilo.ai' \
-v kilo-home:/home/kilo \
-v "$PWD":"/workspace/$(basename "$PWD")" \
-w "/workspace/$(basename "$PWD")" \
kilo-code
Hardening flags¶
| Flag | Purpose |
|---|---|
--security-opt=no-new-privileges |
Prevents escalation through setuid binaries. |
--read-only |
Keeps the root filesystem immutable. |
--tmpfs /tmp:exec and --tmpfs /run |
Provide ephemeral writable locations for the egress-control tooling and runtime files. Kilo's OpenTUI renderer dynamically loads a native library from /tmp, so that mount must be executable. |
--cap-drop=ALL |
Removes Docker's default capabilities. |
NET_ADMIN and NET_RAW |
Required for the default-deny iptables/ip6tables rules and domain allowlists. |
SETUID and SETGID |
Let gosu switch from the setup user to the unprivileged kilo user. |
The kilo-home volume persists Kilo's configuration under
~/.config/kilo, session and credential state, plugin cache, and
user-installed npm/Python tools. Mounting each project below
/workspace/<project-name> keeps its working path distinct while the home
volume is shared across runs.
Custom configuration¶
Kilo's global configuration is below /home/kilo/.config/kilo, but its
single-file bind-mount and mutation behavior has not yet been verified for
this image. See custom configuration files
for the distinction between configuration and mutable state, and local
models for the version-recorded
candidate using a separate KILO_CONFIG input.
Providers, plugins, and egress control¶
The container defaults to deny-all when neither AGENT_ALLOWED_EGRESS nor a
mounted /etc/agent/egress-allowlist.txt file is provided. The mounted file
takes precedence. The sample
agent-images/kilo-code/examples/egress-allowlist.txt permits only Kilo's
hosted AI Gateway, api.kilo.ai.
Mount a host-maintained policy file read-only with
-v "/path/to/egress-allowlist.txt":/etc/agent/egress-allowlist.txt:ro.
Kilo supports its hosted gateway and direct providers. Add only the model
provider endpoint that you configure, along with any intentionally used MCP
server, source-control host, and package registry. Plugins can execute code
and may require package-registry egress; do not install a plugin unless its
source and required access are understood. Use -e 'AGENT_ALLOWED_EGRESS=*'
only when unrestricted egress is intentional; quoting prevents shells such as
zsh from treating * as a filename pattern.
Kilo has its own optional sandboxing and permission controls. They can provide
defence in depth, but the outer kilo-code container is the security boundary:
it owns the project mount, read-only root filesystem, capability reduction, and
egress policy. Do not mount a Docker or Podman socket into this workload.
Gateway-client mode¶
Setting AGENT_GATEWAY_HOST switches the image from its in-container
allowlist to an SSH tunnel through agent-gateway. The
gateway then owns all egress enforcement, so
AGENT_ALLOWED_EGRESS/egress-allowlist.txt are ignored by the workload.
Use the same gateway key, host-key pinning, bootstrap-rule, and Cloudflare
Access configuration described in
the Claude Code gateway guide.
Optional build-time tools¶
Edit packages-apt.txt, packages-npm.txt, tools-uv.txt, or
packages-uv.txt and rebuild to add general-purpose tools. tools-uv.txt
(uv tool install) is for standalone Python CLI tools, each in its own
isolated venv; packages-uv.txt (uv pip install --system) is for plain
importable libraries with no console-script of their own, installed into the
image's system Python instead. No credentials, plugins, skills, or provider
configuration are seeded in the image; Kilo stores user-selected state in
the persisted home volume.
The image also sets UV_LINK_MODE=copy — see claude-code's uv cache
notes for why.