A standard way to declare and run agent containers for your projects. Clone a repo, run one command, and talk to a fully configured AI agent:
- Zero setup:
agentcontainer runbuilds and launches a container with every tool your agent needs, as declared by the project maintainer. - Composable: Your local configuration merges with the project's, so you keep your own agent harness and credentials while inheriting project-specific tooling.
- Reproducible: The container is the environment. Every contributor gets the same agent setup, every time.
cargo install --locked agentcontainer
git clone git@github.com:your-org/your-project
cd your-project
agentcontainer run
# Talk to the LLM.cargo install --locked agentcontainer
agentcontainer reads configuration from the following sources, listed from
lowest to highest priority:
| Source | Path |
|---|---|
| XDG global config | ~/.config/agentcontainer/config.toml |
| Home config | ~/.agentcontainer/config.toml |
| Ancestor configs | {ancestor}/.agentcontainer/config.toml for each ancestor directory from / towards the current working directory (excluding the CWD itself). Closer to / has lower priority. |
| Project config | .agentcontainer/config.toml |
| Local project config | .agentcontainer/config.local.toml |
| Environment variables | AGENTCONTAINER_<KEY> |
| CLI arguments | --<key> flags |
| Key | Default | Description |
|---|---|---|
dockerfile |
.agentcontainer/Dockerfile |
Path to the Dockerfile. |
build_context |
. |
Directory used as the Docker build context. |
build_arguments |
(empty) | Extra --build-arg flags for docker build. See Build arguments. |
pre_build |
(empty) | Paths to executables to run before docker build. See Pre-build hooks. |
project_name |
Last component of the current directory, slugified | Name used in the Docker image tag. |
username |
Current OS user (from whoami) |
Username embedded in the image tag. |
target |
(none) | Docker build --target. When set, appended to the image tag. |
allow_stale |
false |
Use an existing image if the build fails, instead of returning an error. |
force_rebuild |
false |
Rebuild unconditionally, bypassing the staleness check. |
no_build_cache |
false |
Pass --no-cache to docker build. |
no_rebuild |
false |
Skip the build entirely. Errors if no image exists yet. |
volumes |
(empty) | Host-to-container volume mounts. See Volumes. |
environment_variables |
(empty) | Environment variables for the container. See Container environment variables. |
pre_run |
(empty) | Paths to executables to run before docker run. See Pre-run hooks. |
post_run |
(empty) | Paths to executables to run after docker run. See Post-run hooks. |
force_rebuild and no_rebuild are mutually exclusive.
target can be unset by a higher-priority source by setting it to "!". This
suppresses a value inherited from a lower-priority source. For example, if a
global config sets target = "builder", a project config can override it with
target = "!" to build without a target. The same works via environment
variables (AGENTCONTAINER_TARGET="!") and CLI (--target "!").
dockerfile, build_context, and target must not be set to an empty string.
Setting any of them to "" is a validation error. pre_build, pre_run, and
post_run entries must not be empty strings.
A leading ~ in dockerfile or build_context is expanded to the user's home
directory. Only ~ alone or ~/… is expanded; ~user/… and embedded tildes
are left untouched. Relative paths are resolved against the current working
directory. All paths are lexically normalized: . and .. components are
resolved and consecutive slashes are collapsed, without accessing the
filesystem.
The Docker image tag is derived from the resolved configuration:
agentcontainer_<username>_<project_name>:latest
agentcontainer_<username>_<project_name>_<target>:latest # when target is set
username, project_name, and target are all slugified before being
embedded in the tag: lowercased, non-alphanumeric characters replaced with _,
consecutive underscores collapsed, and leading/trailing underscores trimmed.
username and project_name must contain at least one alphanumeric character.
If the slug of either value would be empty, get_config returns an error.
The build_arguments table defines extra --build-arg flags passed to
docker build. Values can be:
- A string: pass this literal value as a build argument.
true: inherit the value from the host environment variable with the same name.false: remove a build argument inherited from a lower-priority config source.
[build_arguments]
USERNAME = "alice"
BUILD_DATE = "2026-03-06"
HOME = true # inherit from host environment
OLD_ARG = false # suppress from a lower-priority sourceOn the CLI, use --build-arg (repeatable):
# Set a literal value.
agentcontainer build --build-arg USERNAME=alice
# Inherit from the host environment.
agentcontainer build --build-arg HOME
# Remove a build argument inherited from config files.
agentcontainer build --build-arg '!OLD_ARG'Build argument keys must be valid POSIX identifiers: start with a letter or underscore, followed by ASCII letters, digits, or underscores.
The pre_build option specifies a list of paths to executables that run before
docker build. Hooks form a pipeline: the first hook receives the hookable
arguments computed from the configuration (i.e. --build-arg entries), and
each subsequent hook receives the output of the previous one. The final result
is injected into the docker build command after all built-in flags but before
the build context. Built-in flags managed by agentcontainer (--file,
--target, --tag, --no-cache, build context) are not exposed to hooks.
Lists from multiple config sources are concatenated (lower-priority first). For
example, a hook defined in config.toml runs before one added via
config.local.toml or the CLI.
This provides a way to dynamically compute Docker build flags based on the host environment.
Note: Pre-build hooks run unconditionally whenever a build or run
command is invoked, even if the build is ultimately skipped (e.g. because the
image is already up to date or --no-rebuild is set). Keep this in mind if a
hook is expensive or has side effects.
Each hook:
- Receives the path to a temporary file as its first argument (
$1). The file contains the current arguments as a TOML document with anargskey (e.g.args = ["--build-arg", "FOO=bar"]). - Must print a TOML document with the same shape to stdout (e.g.
args = ["--build-arg", "FOO=bar", "--label", "version=1"]). - Must exit with status 0.
- Must produce valid UTF-8 output.
Example hook script (requires toml from toml-cli and jq):
#!/bin/sh
set -euo pipefail
input_file="$1"
# Read the current args from the input file.
existing=$(toml get "$input_file" args)
# Build extra entries.
extra=$(jq -n \
--arg a1 "--build-arg" --arg v1 "BUILD_DATE=$(date "+%Y-%m-%d")" \
'[$a1, $v1]')
# Merge and output.
merged=$(jq -cn --argjson existing "$existing" --argjson extra "$extra" \
'$existing + $extra')
echo "args = ${merged}"A leading ~ in each path is expanded to the user's home directory. Only ~
alone or ~/… is expanded; ~user/… and embedded tildes are left untouched.
Relative paths are resolved against the current working directory. All paths
are lexically normalized: . and .. components are resolved and consecutive
slashes are collapsed, without accessing the filesystem.
In TOML configuration:
pre_build = ["~/hooks/pre-build.sh"]
pre_build = [
"scripts/pre-build.sh",
] # resolved relative to the current working directory
pre_build = ["hooks/a.sh", "hooks/b.sh"] # multiple hooks in one sourceOn the CLI (repeatable):
agentcontainer build --pre-build ~/hooks/pre-build.sh
agentcontainer build --pre-build hooks/a.sh --pre-build hooks/b.shThe volumes table maps container paths to host paths. In TOML, each key is a
container path and the value is either:
- A string: an explicit host path to mount at the container path.
true: mount at the same path in the container as on the host (the key is used as both host and container path).false: remove a volume inherited from a lower-priority config source.
A leading ~ in container-path keys and host-path values is expanded to the
user's home directory before config sources are merged. This means ~/.ssh and
/home/alice/.ssh from different sources are treated as the same volume during
priority resolution. Only ~ alone or ~/… is expanded; ~user/… and
embedded tildes are left untouched.
Relative host paths that look like filesystem paths (start with . or contain
/) are resolved against the current working directory. Plain names without a
. prefix or / (e.g. my_volume) are treated as Docker volume names and
left unchanged. All resolved paths are lexically normalized: . and ..
components are resolved and consecutive slashes are collapsed, without
accessing the filesystem.
[volumes]
"/workspace" = "~/projects/myproject"
"/data" = "/mnt/shared-data"
"/app" = "./src" # resolved relative to the current working directory
"~/.ssh" = true # mount at the same path inside the container
"/cache" = "my_cache_volume" # Docker volume name, left unchanged
"/unwanted" = false # suppress a volume defined in a lower-priority sourceOn the CLI, use --volume (or -v) (repeatable):
# Mount a host path into the container.
agentcontainer build --volume '~/projects/myproject:/workspace'
# Mount at the same path inside the container (same-path shorthand).
agentcontainer build -v /home/alice/.ssh
# Remove a volume inherited from config files.
agentcontainer build --volume '!/unwanted'The environment_variables table defines environment variables to pass into
the container. Values can be:
- A string: pass this literal value.
true: inherit the variable from the host environment.false: remove a variable inherited from a lower-priority config source.
[environment_variables]
EDITOR = "nvim"
SSH_AUTH_SOCK = true # inherit from host
OLD_VAR = false # suppress from a lower-priority sourceOn the CLI, use --env (or -e) (repeatable):
# Set a literal value.
agentcontainer build --env EDITOR=nvim
# Inherit from the host.
agentcontainer build --env SSH_AUTH_SOCK
# Remove a variable inherited from config files.
agentcontainer build -e '!OLD_VAR'Variable keys must be valid POSIX identifiers: start with a letter or underscore, followed by ASCII letters, digits, or underscores.
The pre_run option specifies a list of paths to executables that run before
docker run. Hooks form a pipeline: the first hook receives the hookable
arguments computed from the configuration (i.e. --volume and --env
entries), and each subsequent hook receives the output of the previous one. The
final result is injected into the docker run command after all built-in flags
but before the image name. Built-in flags managed by agentcontainer (--init,
--rm, --tty, --interactive, --user, --group-add, --name,
--workdir, the current-directory volume, the worktree volume, the image name,
and container arguments) are not exposed to hooks.
Lists from multiple config sources are concatenated (lower-priority first). For
example, a hook defined in config.toml runs before one added via
config.local.toml or the CLI.
This provides a way to dynamically compute Docker flags at runtime based on the host environment.
Each hook:
- Receives the path to a temporary file as its first argument (
$1). The file contains the current arguments as a TOML document with anargskey (e.g.args = ["--volume", "/host:/container"]). - Must print a TOML document with the same shape to stdout (e.g.
args = ["--volume", "/host:/container", "--network", "host"]). - Must exit with status 0.
- Must produce valid UTF-8 output.
Example hook script (requires toml from toml-cli and jq):
#!/bin/sh
set -euo pipefail
input_file="$1"
# Read the current args from the input file.
existing=$(toml get "$input_file" args)
# Build extra entries.
extra=$(jq -n \
--arg a1 "--network" --arg v1 "host" \
'[$a1, $v1]')
# Merge and output.
merged=$(jq -cn --argjson existing "$existing" --argjson extra "$extra" \
'$existing + $extra')
echo "args = ${merged}"A leading ~ in each path is expanded to the user's home directory, just like
for volume paths and pre_build. Only ~ alone or ~/… is expanded;
~user/… and embedded tildes are left untouched. Relative paths are resolved
against the current working directory. All paths are lexically normalized: .
and .. components are resolved and consecutive slashes are collapsed, without
accessing the filesystem.
In TOML configuration:
pre_run = ["~/hooks/pre-run.sh"]
pre_run = [
"scripts/pre-run.sh",
] # resolved relative to the current working directory
pre_run = ["hooks/a.sh", "hooks/b.sh"] # multiple hooks in one sourceOn the CLI (repeatable):
agentcontainer run --pre-run ~/hooks/pre-run.sh
agentcontainer run --pre-run hooks/a.sh --pre-run hooks/b.shThe post_run option specifies a list of paths to executables that run after
docker run. Hooks form an output pipeline: the first hook receives the
captured stdout from docker run, and each subsequent hook receives the output
of the previous one. The final output is written to agentcontainer's stdout.
When post_run hooks are configured, docker run is spawned as a child
process instead of replacing the current process via exec. This means stdout
is captured rather than inherited. Stderr is always inherited, so container
diagnostics and error messages are visible to the user in real-time.
Note: When post_run hooks are present, TTY allocation (--tty and
--interactive) is disabled because a pseudo-TTY merges stdout and stderr into
a single stream, which would corrupt the captured output. This means the
container runs in non-interactive mode.
Lists from multiple config sources are concatenated (lower-priority first). For
example, a hook defined in config.toml runs before one added via
config.local.toml or the CLI.
This provides a way to post-process the container's output, for example to filter, transform, or reformat it before presenting it to the user.
Each hook:
- Receives the path to a temporary file as its first argument (
$1). The file contains the stdout from the previous stage (or fromdocker runitself for the first hook). - Must print the transformed output to its own stdout.
- Must exit with status 0.
Example hook script:
#!/bin/sh
set -euo pipefail
input_file="$1"
# Strip ANSI escape codes from the container output.
sed 's/\x1b\[[0-9;]*m//g' "$input_file"A leading ~ in each path is expanded to the user's home directory, just like
for volume paths, pre_build, and pre_run. Only ~ alone or ~/… is
expanded; ~user/… and embedded tildes are left untouched. Relative paths are
resolved against the current working directory. All paths are lexically
normalized: . and .. components are resolved and consecutive slashes are
collapsed, without accessing the filesystem.
In TOML configuration:
post_run = ["~/hooks/post-run.sh"]
post_run = [
"scripts/post-run.sh",
] # resolved relative to the current working directory
post_run = ["hooks/a.sh", "hooks/b.sh"] # multiple hooks in one sourceOn the CLI (repeatable):
agentcontainer --post-run ~/hooks/post-run.sh run
agentcontainer --post-run hooks/a.sh --post-run hooks/b.sh runWhen running a container, the name is derived from the project name and a random numeric suffix:
agentcontainer_<project_name>_<suffix>
The slugified project name is truncated to 41 characters (with any trailing underscore removed after truncation) so that the full container name never exceeds Docker's 63-character limit for container names.
dockerfile = ".agentcontainer/Dockerfile"
build_context = "."
project_name = "myproject"
username = "alice"
[volumes]
"/workspace" = "~/projects/myproject"
"~/.ssh" = true # same path on host and in container
[environment_variables]
EDITOR = "nvim"
SSH_AUTH_SOCK = trueEach configuration key maps to an AGENTCONTAINER_<KEY> environment variable,
where <KEY> is the uppercase version of the key name. Values are parsed as
TOML. For example:
AGENTCONTAINER_DOCKERFILE=".agentcontainer/Dockerfile"
AGENTCONTAINER_BUILD_CONTEXT="."
AGENTCONTAINER_BUILD_ARGUMENTS='{BUILD_DATE = "2026-03-06", HOME = true}'
AGENTCONTAINER_PRE_BUILD='["~/hooks/pre-build.sh"]'
AGENTCONTAINER_ALLOW_STALE=true # or `false`
AGENTCONTAINER_VOLUMES='{"/workspace" = "~/projects/myproject", "~/.ssh" = true}'
AGENTCONTAINER_ENVIRONMENT_VARIABLES='{EDITOR = "nvim", SSH_AUTH_SOCK = true}'
AGENTCONTAINER_PRE_RUN='["~/hooks/pre-run.sh"]'
AGENTCONTAINER_POST_RUN='["~/hooks/post-run.sh"]'Print the resolved configuration, with all sources merged, as TOML. The output includes a comment header listing all configuration sources in priority order (lowest to highest), showing which config files were found on disk alongside built-in defaults, environment variables, and CLI arguments.
agentcontainer config
Build the agent container Docker image.
agentcontainer build
The build is skipped if the image is already up to date. A rebuild is triggered when any of the following is true:
- No image exists yet.
- The Dockerfile was modified after the image was last tagged.
- The image was last tagged before the start of today (local time).
force_rebuildis set.
Run the agent container. The image is automatically built (or rebuilt if stale)
before starting the container. When no post_run hooks are configured, this
replaces the current process with docker run via exec. When post_run
hooks are present, docker run is spawned as a child process so its stdout can
be captured and piped through the hook pipeline.
agentcontainer run [-- <container-args>...]
Arguments after -- are passed through to the container's entrypoint.
Everything before -- is parsed by agentcontainer itself and will error on
unrecognized flags.
# Run interactively with no extra arguments.
agentcontainer run
# Pass arguments to the container entrypoint.
agentcontainer run -- --print --output-format json
# Global flags still go before the subcommand.
agentcontainer --project-name foo run -- --helpThe build step honors all build-related configuration keys (allow_stale,
force_rebuild, no_build_cache, no_rebuild, etc.). If the image is already
up to date, the build is skipped and the container starts immediately.
The container is started with:
--init: the container uses an init process.--rm: the container is automatically removed on exit.- UID/GID mapping: the container runs as the current user and group, with
group
0added via--group-add. - Current directory volume: the working directory is mounted into the container at the same path and set as the container's working directory.
- Git worktree volume: if the current directory is a linked Git worktree, the main worktree root is also mounted so that Git objects are accessible.
- Configured volumes and environment variables: as defined in the configuration.
- TTY mode:
--tty(allocate pseudo-TTY) and--interactive(keep stdin open) are only added when standard input is a TTY. This means piped or scripted invocations won't cause Docker to hang or emit spurious warnings.
This project is the example.
First, we have .agentcontainer/Dockerfile. It
installs all the tools the agent will need to interact with this project during
development. For example:
docker, so the agent can interact with the development container.gitandghto interact with Git history and GitHub PRs.- Prepares the container with the current user's ID and group ID to avoid permission problems.
- Mechanisms to bust image caching to easily update the agent harness periodically.
- Install an agent harness (Claude Code).
And the .agentcontainer/config.toml file
configures scripts that allow customization of Docker build arguments and
Docker run arguments.
- The
pre_buildhook takes care of passing the current user's ID and group ID to the image, and today's date to bust the cache daily to update the agent harness. - The
pre_runhook takes care of exposing the right Docker socket for Docker-in-Docker functionality. - The
post_runhook processes Claude Code's JSON output from--print --output-format jsonmode: it extracts the result text, and appends a cost and duration summary. This hook is not configured inconfig.tomlbecause it is only useful for non-interactive runs. Instead, pass it via--post-runwhen invoking the container in print mode:agentcontainer \ --post-run .agentcontainer/process_claude_output.sh \ run \ -- --print --output-format json "Your prompt here"
agentcontainer always mounts the project into the container, and it
automatically detects if it is in a Git worktree and, if so, makes sure to also
mount the main worktree to make sure Git commands will be available to the
agent.
The CLAUDE.md tells the agent about the development tools it has
available (one to run the linters, and one to run the tests), and those tools
take care of everything for the agent (building the development container and
running it if needed).
The objective is that all you have to do is download the project, run
agentcontainer run, and start talking to the LLM. Describe the change you
want to make, and off it goes all the way to making a commit. If you leverage
the flexible configuration from above by sharing a GH_TOKEN, it could even
push the PR for you.
The pre_build and pre_run scripts could do anything you need. For example:
- Dynamically customize your
~/.claude/settings.jsonfile depending on each project's needs, push the settings to a temporary file, and mount that temporary file into the agent container. - Fetch your LLM provider API key from your secret manager (1Password,
LastPass, BitWarden,
age) and inject it into the agent container via--envor--env-file. - Fetch your GitHub token and inject it into the agent container as
GH_TOKEN.
in_buildhooks: Executables to be injected into the build context and executed during the build process to allow customizing the image itself.
- Fork this project.
- Download it locally.
- If you want assistance from Claude Code, run
agentcontainer run. - Make your changes.
- Push your changes to your forked repository.
- Create a PR.
This project is licensed under the MIT license.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this repository by you shall be licensed as MIT, without any additional terms or conditions.
