Skip to content

The adal container

adal packages AdaL, SylphAI's 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.

The image uses the documented npm fallback, pinned to @sylphai/adal-cli@1.7.1. The package requires Node.js 20 or newer and selects its Linux x64 or arm64 runtime during the image build. The native installer is deliberately not used: it is an unpinned curl | bash installer that manages updates outside the image build.

Build

docker build --build-context shared=agent-images/shared \
  -t adal agent-images/adal

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 adal:$(whoami) agent-images/adal

Dockerfile splits into a base stage that installs everything — apt/Node.js/uv baseline, packages-apt.txt/packages-uv.txt, AdaL 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 (adal:alice, adal:bob) rather than overwriting a shared adal tag — and use that same tag, plus a per-user home volume (adal-home-alice rather than the shared adal-home), in the run command below. The shell-shortcut functions do both substitutions automatically from a single CONTAINED_ADAL_TAG variable; if invoking docker run directly instead, substitute your tag into both the adal-home volume name and the trailing adal image reference in the command below — copying it unchanged still runs the shared adal:latest.

Run

AdaL opens a browser on its first run for authentication. Keep its state in the named home volume; do not put credentials in an image, build argument, or checked-in file. The initial egress rule permits the documented AdaL sign-in host. Add only hosts required by a feature you intentionally enable.

docker run -it --rm \
  --security-opt=no-new-privileges \
  --read-only --tmpfs /tmp --tmpfs /run \
  --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID \
  -e 'AGENT_ALLOWED_EGRESS=adal.sylph.ai' \
  -v adal-home:/home/adal \
  -v "$PWD":"/workspace/$(basename "$PWD")" \
  -w "/workspace/$(basename "$PWD")" \
  adal

Hardening flags

Flag Purpose
--security-opt=no-new-privileges Prevents escalation through setuid binaries.
--read-only Keeps the root filesystem immutable.
--tmpfs /tmp and --tmpfs /run Provide ephemeral writable locations for the egress-control tooling and runtime files.
--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 adal user.

The adal-home volume persists AdaL's credentials, settings, session history, MCP OAuth state, skills, plugin cache, and user-installed npm/Python tools under ~/.adal. It is shared state: mount a separate volume when different projects or trust domains should not share that state.

Egress and credentials

Without AGENT_ALLOWED_EGRESS or a mounted /etc/agent/egress-allowlist.txt, the image denies all outbound traffic. The sample agent-images/adal/examples/egress-allowlist.txt contains only adal.sylph.ai, the documented sign-in host. It is a starting point rather than a broad service policy: observe and add any further required AdaL first-party hostname when a tested version needs it.

AdaL features can initiate connections to substantially different services:

  • BYOAK connects directly to the selected Anthropic, OpenAI, or Google provider. Add only that provider's API host and pass a key at runtime; AdaL stores keys in ~/.adal/settings.json.
  • MCP supports remote HTTP/SSE servers and local stdio processes. Allow each remote server explicitly. A local MCP server runs inside this same constrained container; its package-registry access is not granted by default.
  • Skills and plugins can clone marketplace repositories. Do not seed a marketplace or plugin in the image; add a source-control host only when the user elects to install one.
  • Web search, Browser Use, image/video features, and custom tools may each contact separate user-selected services. They require their own narrow egress entries and should not enlarge the default policy.

Mount a host-maintained allowlist read-only when the set is larger than one host:

docker run -it --rm \
  --security-opt=no-new-privileges \
  --read-only --tmpfs /tmp --tmpfs /run \
  --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID \
  -v adal-home:/home/adal \
  -v "$PWD":"/workspace/$(basename "$PWD")" \
  -v "$PWD/agent-images/adal/examples/egress-allowlist.txt":/etc/agent/egress-allowlist.txt:ro \
  -w "/workspace/$(basename "$PWD")" \
  adal

Do not use -e 'AGENT_ALLOWED_EGRESS=*' unless unrestricted egress is an intentional trade-off; quoting prevents shells such as zsh from treating * as a filename pattern.

Browser Use and local models

AdaL documents Chromium as a manual Linux prerequisite for Browser Use. It is not included in the baseline image: installing it is optional and must be tested in a derived image or by editing packages-apt.txt. Do not weaken the browser sandbox with a --no-sandbox workaround. Browser Use can operate on real sites and accounts, so use test accounts and explicitly allow each site that it must reach.

AdaL's Ollama support is also optional. This image does not install or run an Ollama server or model. Run it separately and provide a deliberately reachable, allowlisted endpoint; a service listening on the Docker host is not implicitly reachable from the container. See the version-recorded candidate in local models.

Project code, permissions, and hooks

AdaL can auto-load .adal/tools.py, project skills, and AGENTS.md from the mounted project. These are code and instructions supplied by that project, not trusted image content. Lifecycle hooks are loaded from ~/.adal/settings.json and execute as the unprivileged container user. Keep the home volume and project mount within an appropriate trust boundary.

Interactive AdaL asks for approval before modifications and shell commands. For headless use, prefer a narrow explicit tool set, for example:

adal -q "review the change" --enabled-default-tools "Read,Search" --yolo

--yolo auto-approves every enabled tool. It does not relax the container's filesystem, capability, or egress boundaries, but it can freely modify the mounted project and invoke enabled local tools.

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, extensions, skills, plugins, browser, or local-model runtime is seeded in the image.

The image also sets UV_LINK_MODE=copy — see claude-code's uv cache notes for why.