Skip to main content
Guides

Lifecycle hooks

Hooks let you run commands at specific points in the container lifecycle: after the image is built, before the main command runs, and after it completes. This guide covers each hook type, what it has access to, and practical patterns for common setups.

Prerequisites

  • A working Moat installation with Docker or Apple container runtime
  • An agent.yaml file in your project directory

Hook types

Moat supports three lifecycle hooks, each running at a different stage:

HookWhen it runsUserCachedWorkspace available
post_build_rootDuring image build, after dependenciesrootYesNo
post_buildDuring image build, after post_build_rootmoatuserYesNo
pre_runEvery container start, before commandmoatuserNoYes

post_build_root

Runs as root during image build. Use for system-level setup that requires elevated privileges: installing system packages, modifying /etc files, or kernel tuning.

hooks:
  post_build_root: apt-get update -qq && apt-get install -y -qq figlet

post_build

Runs as moatuser during image build. Use for user-level image setup: configuring tools, setting git defaults, or compiling assets.

hooks:
  post_build: git config --global core.autocrlf input

pre_run

Runs as moatuser in /workspace on every container start, before the main command. Use for workspace-level setup that needs your project files: installing dependencies, running codegen, or building assets.

hooks:
  pre_run: npm install

pre_run runs before any command, including when moat claude or moat codex overrides command.

Execution order

The full container lifecycle proceeds in this order:

Image build (cached):
  1. Dependencies installed (from dependencies:)
  2. post_build_root runs as root in /workspace
  3. post_build runs as moatuser in /workspace

Container start (every run):
  4. Workspace mounted
  5. pre_run runs as moatuser in /workspace
  6. command (or agent command) executes

Build hooks (steps 1-3) are baked into the image layer and cached. Runtime hooks (steps 5-6) run on every container start.

Build time vs runtime

Build hooks (post_build, post_build_root) run during image build. They cannot access workspace files because the workspace is not mounted yet. They have access to the container filesystem and network.

pre_run runs at container start when the workspace is mounted. It has access to your project files, environment variables, and secrets.

Choose the right hook based on what you need:

TaskHookWhy
Install system packagespost_build_rootNeeds root, cached in image
Configure git defaultspost_buildUser-level, cached in image
Install project dependenciespre_runNeeds workspace files
Run database migrationspre_runNeeds workspace and network
Compile a binarypost_build or pre_runpost_build if source is in image; pre_run if source is in workspace

Configuration examples

Node.js project

Install project dependencies on every start. Package managers like npm install are fast no-ops when node_modules is current.

name: my-node-app

dependencies:
  - node@20
  - git

hooks:
  pre_run: npm install

command: ["npm", "start"]

Python project with system dependencies

Install a system library at build time (cached), then install project dependencies at runtime.

name: my-python-app

dependencies:
  - python@3.11

hooks:
  post_build_root: apt-get update -qq && apt-get install -y -qq libpq-dev
  pre_run: pip install -r requirements.txt

command: ["python", "app.py"]

Go project with tools

Configure git and install linting tools at build time. Build the project from the workspace at runtime.

name: my-go-service

dependencies:
  - go@1.22
  - golangci-lint
  - git

hooks:
  post_build: git config --global core.autocrlf input && git config --global pull.rebase true
  pre_run: go build ./...

command: ["./my-go-service"]

Multi-step build hooks

Chain commands with && for multi-step setup within a single hook:

hooks:
  post_build_root: apt-get update -qq && apt-get install -y -qq curl gnupg
  post_build: git config --global core.autocrlf input && git config --global pull.rebase true
  pre_run: npm install && npm run build

Note: Git identity (user.name and user.email) is imported automatically from the host when git is listed as a dependency. Use a post_build hook only if you need to override the host identity or set other git configuration.

Claude Code agent with setup

name: my-agent

dependencies:
  - node@20
  - claude-code
  - git
  - gh

grants:
  - anthropic
  - github

hooks:
  post_build_root: apt-get update -qq && apt-get install -y -qq jq
  pre_run: npm install

Build caching

Build hooks (post_build, post_build_root) are Dockerfile RUN commands. The build system caches each layer, so they only re-run when:

  • The command string changes in agent.yaml
  • You use --rebuild to force a fresh build
  • A preceding layer changes (new dependency, base image update, etc.)

pre_run is not cached. It runs on every container start.

To force re-running build hooks:

$ moat run --rebuild ./my-project

Moving work to build hooks for speed

If pre_run takes a long time on every start, consider whether part of the work can move to a build hook. For example, installing a Python package that compiles C extensions is slow in pre_run but fast on subsequent runs when cached in post_build.

However, build hooks cannot access your workspace files. Only packages or tools that don’t depend on your project code can be installed at build time. For project-specific dependencies (those listed in package.json, requirements.txt, etc.), use pre_run. Workspace-aware package managers like npm install and pip install skip already-installed packages, keeping repeated pre_run execution fast.

Troubleshooting

Hook fails during image build

Build hooks run without access to workspace files. If a command references a file from your project, move it to pre_run instead.

Error: post_build command failed: npm install

npm install needs package.json from the workspace. Use pre_run instead:

hooks:
  # Wrong: workspace not available during build
  # post_build: npm install

  # Correct: workspace is mounted at runtime
  pre_run: npm install

Command not found in hook

The command must be available in the image at the time the hook runs. If a build hook references a tool, ensure it is either:

  • Part of the base image
  • Installed by a preceding step (dependency or earlier hook)

post_build_root runs before post_build, so system packages installed in post_build_root are available in post_build. Both build hooks run after dependencies are installed.

dependencies:
  - node@20       # Installed before hooks run

hooks:
  post_build_root: apt-get update -qq && apt-get install -y -qq imagemagick
  post_build: node --version   # node is available from dependencies
  pre_run: npx prisma generate  # npx is available from node dependency

pre_run slows down every start

If pre_run performs expensive work, consider:

  1. Moving static setup to build hooks — System packages and tool configuration belong in post_build_root or post_build where they are cached.
  2. Using workspace-aware commandsnpm install, pip install, and go build skip work when outputs are current. These are safe and fast in pre_run.
  3. Adding guards — For custom scripts, check whether the work is already done:
hooks:
  pre_run: "test -f .setup-done || (npm install && npm run codegen && touch .setup-done)"

Hook runs but changes are lost

Build hooks modify the image layer. If you install something in post_build but it is missing at runtime, check that:

  • The installation path is not inside /workspace (which gets replaced by the mounted workspace at runtime)
  • The command ran successfully during build (check build output with --verbose)