The Construct Image
What's inside the shared base image that every agent starts from
This page has two readers. Role authors (user-facing) read the
top half — what is in the construct, what tools come for free,
how to extend it from a role's Dockerfile. Contributors
(internals-facing) read the lower half — how to build, validate,
and publish the construct image itself (mise run construct-build-local,
CI workflow, advanced publish rehearsal). Operators using jackin'
as a product do not need to read either half; jackin load pulls
and updates the construct automatically.
What is the construct?
projectjackin/construct:trixie is the shared base Docker image for every agent — the foundation layer that every role extends. It provides system tools, shell environment, and container infrastructure, kept in one image so every agent inherits the same baseline.
Every agent Dockerfile starts from the construct:
FROM projectjackin/construct:0.4-trixieWhat's inside
The construct is built on Debian Trixie and includes:
System tools
| Package | Purpose |
|---|---|
bash | Default shell for scripts |
zsh + Oh My Zsh | Default interactive shell for the agent user. Oh My Zsh runs with ZSH_THEME="" so the git plugin, autosuggestions plugin, and OSC 0/2 + OSC 7 auto-title hooks are active without overriding the Starship prompt. |
fish | Opt-in alternative shell. Pre-configured ~/.config/fish/config.fish initialises Starship and emits the same OSC 0/2 + OSC 7 pane title escapes as zsh, so pane border titles render identically. Enter with fish from a default zsh pane, or set as the login shell with sudo chsh -s /usr/bin/fish agent. |
git + Git LFS | Source control with large file support |
curl | HTTP client |
jq + yq | JSON and YAML processors |
openssh-client | SSH for git operations |
sudo | Privilege escalation (passwordless for agent user) |
tree | Directory visualization |
Search tools
| Package | Purpose |
|---|---|
ripgrep (rg) | Fast regex search |
fd-find (fd) | Fast file finder |
fzf | Fuzzy finder for interactive selection |
Development infrastructure
| Package | Purpose |
|---|---|
mise | Polyglot language version manager |
| Docker CLI + Compose | Container operations via DinD |
GitHub CLI (gh) | Repository, PR, and issue operations |
| Starship prompt | Informative terminal prompt |
User environment
The construct creates an agent user with:
- Home directory at
/home/agent - Zsh as the default shell, with fish available as an opt-in alternative (
fishfrom zsh, orsudo chsh -s /usr/bin/fish agentto change the login shell) - Passwordless sudo access
- Oh My Zsh sourced with
ZSH_THEME=""so the git plugin, autosuggestions plugin, and OSC 0/2 + OSC 7 title hooks are active without competing with Starship for the prompt - Starship prompt configured for both zsh and fish so the prompt surface stays the same when switching shells
- Pane border title (
user@host:cwd) emitted via OSC 0/2 on every prompt — the jackin-capsule multiplexer reads it and renders it as the pane title (the same mechanism zellij uses) - mise shims in
$PATHfor both shells
How it's built
The construct source starts at docker/construct/Dockerfile in the jackin' repository. The declarative build definition lives in docker-bake.hcl at the repo root, and the supported command surface is the construct-* mise tasks defined in mise.toml, backed by the crates/jackin-xtask/src/construct.rs Rust task runner.
docker/construct/versions.env remains the source of truth for the pinned tirith, shellfirm, and MISE_VERSION build args used by docker/construct/Dockerfile. mise is installed with the official standalone installer during the Docker build, which downloads the pinned version's release asset from GitHub — where every version stays available, so the pin never goes stale. The pinned version gives Buildx a stable cache key, and Renovate bumps invalidate the mise install layer when upstream publishes a new release.
Build workflow
Use the construct-* mise tasks for day-to-day construct work. Docker Bake is the underlying build engine, but contributors should treat mise run as the supported command surface.
Local validation
Before opening a pull request for construct changes, validate the image locally:
mise run construct-init-buildx
mise run construct-build-localBy default, local validation loads the image into your Docker daemon as jackin-local/construct:trixie, so it does not silently replace the canonical projectjackin/construct:trixie base image that normal jackin workflows consume.
To test jackin load against your local construct build, set JACKIN_CONSTRUCT_IMAGE after building:
mise run construct-init-buildx
mise run construct-build-local
export JACKIN_CONSTRUCT_IMAGE="jackin-local/construct:trixie"With JACKIN_CONSTRUCT_IMAGE set, jackin' validates role Dockerfiles against the canonical image name as usual (role repos reference a versioned construct tag such as projectjackin/construct:0.4-trixie), but substitutes your local image at derived-build time so docker build uses jackin-local/construct:trixie as the actual base.
Use mise run construct-inspect to print the fully resolved Bake config without building the image.
If builder state gets stale or confusing, run mise run construct-doctor-buildx to inspect it or mise run construct-reset-buildx to recreate it. Set BUILDX_BUILDER before running the task if you want to isolate this workflow under a different local builder name.
Platform-specific debugging
To force a specific target architecture during local debugging:
mise run construct-build-platform amd64
mise run construct-build-platform arm64These commands work when your buildx builder supports the target platform natively or through emulation.
Advanced publish rehearsal
If you need to rehearse the release path against a temporary registry, point REGISTRY_IMAGE at your own namespace instead of the canonical projectjackin/construct repository:
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER mise run construct-push-platform amd64
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER mise run construct-push-platform arm64
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER mise run construct-publish-manifestmise run construct-push-platform and mise run construct-publish-manifest intentionally refuse to publish to projectjackin/construct unless they are running in CI.
CI behavior
GitHub Actions reuses the same mise run commands on native ubuntu-24.04 and ubuntu-24.04-arm runners. Construct CI validates broad build plumbing when changes touch docker/construct/**, docker-bake.hcl, the crates/jackin-xtask/ task runner, or .github/workflows/construct.yml. It only treats a run as a construct-image publish candidate when changes touch inputs baked into the image itself: docker-bake.hcl, docker/construct/Dockerfile, docker/construct/versions.env, docker/construct/zshrc, or docker/construct/fish-config.fish.
Pull requests, plus manual workflow runs against non-main branches, build both architectures natively without pushing images. Those validation jobs use the GitHub Actions cache backend; the cache key includes the pinned MISE_VERSION, so a Renovate mise bump invalidates the mise install layer while unrelated earlier layers can still be reused. They still do not receive Docker Hub credentials.
Pushes to main, plus manual workflow runs against main, publish each platform by digest first, then assemble the final multi-platform manifest from those digests when the run is a construct-image publish candidate. Build-plumbing-only changes still validate the construct workflow, but they skip the immutable-version guard and registry publish path because no baked image input changed. The registry cache uses the same pinned MISE_VERSION keying behavior as PR builds. That keeps the public tag surface limited to the canonical release tags instead of leaving permanent public -amd64 and -arm64 tags behind.
Published images also carry explicit BuildKit provenance and SBOM attestations.
The published tags are:
projectjackin/construct:trixie— the stable tagprojectjackin/construct:trixie-<sha>— commit-specific tag
The derived image layer
When you load an agent, jackin' doesn't use the construct directly. It generates a derived Dockerfile that adds:
- User remapping — adjusts the
agentuser's UID/GID to match your host user - Agent installation — copies jackin's cached agent binaries for every agent declared in the role manifest
- Plugin installation — any Claude plugins declared in the role manifest
- Runtime entrypoint + setup bridge — the script delegates deterministic setup to
jackin-capsule runtime-setup, then keeps the shell-native work: sourcing role hooks andexec-ing the selected agent. The Rust setup path handles one-time container git/GitHub initialization, the shared git trailer hook, durable agent-home seeding, auth handoff refreshes, and Claude MCP registration. It wires up GitHub ifghis already authenticated; it does not performgh auth login, so an unauthenticatedghis left alone with a notice.
Every load re-runs docker build on the derived Dockerfile. Docker's layer cache makes most subsequent loads fast, but there is no separate "skip build if image exists" shortcut. Before the build, jackin' resolves the latest agent binary metadata and stores downloaded binaries under its host cache. Passing --rebuild invalidates agent installation layers; unchanged versions are copied from the local cache instead of downloaded again.
Extending the construct
Role repos add their tools on top of the construct. The construct provides the foundation — agents provide the specialization:
┌─────────────────────────────────┐
│ Derived Layer (jackin-managed) │ Agent CLIs, entrypoint, user mapping
├─────────────────────────────────┤
│ Agent Layer (your Dockerfile) │ Rust, Node, Python, custom tools
├─────────────────────────────────┤
│ Construct (shared base) │ Debian, git, Docker CLI, mise, zsh
└─────────────────────────────────┘This layered approach means:
- Agent authors focus on their tools, not infrastructure
- The construct can be updated independently (security patches, new tools)
- Docker layer caching makes builds fast