# Deploy Hankweave without source-tree assumptions This page takes you from a downloaded fixture bundle to a verified, containerized Hankweave deployment: a consumer image built from the published npm package, a contributor image built from source, and the wiring for inputs, state, ports, CI and health checks that makes either one safe to run. Each recipe ends with a concrete check so you can confirm the step worked before moving on. Two assumptions trip people up most often, and the page is organized around avoiding them. First, a deployed image is not a source checkout: the installed `hankweave` bin is the entrypoint, and source-tree commands do not apply. Second, a container that starts is not a container that ran fresh work: exit codes, lock files and resume behavior all need explicit handling, which the later sections cover. For Kubernetes Jobs, start with the batch-process model in [Deployment model](/0.10.0/files/integrate/deployment-model) and return here for the image and flag details. ## Choose the image that matches your job A consumer image installs the unscoped npm package `hankweave`; a contributor image builds `SouthBridgeAI/hankweave-runtime` from source. Pick the consumer image unless you are developing Hankweave itself. Do not use `bun run server/index.ts` in a consumer image: that entrypoint presumes the source tree. Invoke the installed `hankweave` bin with an explicit hank and data path; there is no `run` subcommand. The recipes below use the [0.10.0 fixture bundle](/content-assets/cf45dff5691c48c0/fixtures/0.10.0/hankweave-fixtures-0.10.0.tar.gz), so download it before following them. The archive already contains one top-level `hankweave-fixtures-0.10.0/` directory: extract it in its parent, then enter the extracted directory, and do not create another directory with that name first. The recipes use `minimal-single-provider/hank.json` and `minimal-single-provider/data/`, plus [`container-smoke/Dockerfile`](/content-assets/cf45dff5691c48c0/fixtures/0.10.0/files/container-smoke/Dockerfile). Export `ANTHROPIC_API_KEY` before a run; do not put provider keys in the image. ```sh # Run in the directory containing the downloaded archive. tar -xzf hankweave-fixtures-0.10.0.tar.gz cd hankweave-fixtures-0.10.0 export ANTHROPIC_API_KEY=your-key ``` ## Build the consumer image from npm The consumer image needs a Node image with Node `>=22.19.0`, or Bun as the runner. Install `git` as well; checkpoints – git-backed recovery points – use the `git` executable. Pin the package version in the image so its behavior and model spellings match this documentation: ```sh npm install -g hankweave@0.10.0 ``` With Bun, pin the version on each invocation instead: ```sh bunx hankweave@0.10.0 ``` Since 0.9.0, npm trusted publishing and provenance allow an image build to verify the package with `npm audit signatures` when the npm setup supports that check. Rather than write a Dockerfile from scratch, we can use the smoke image shipped in the fixture bundle. It installs the pinned package, creates the working and output directories, switches to the non-root `node` user and sets the installed bin as the entrypoint: ```dockerfile FROM node:22-bookworm-slim ARG HANKWEAVE_VERSION=0.10.0 RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/* && npm install -g hankweave@${HANKWEAVE_VERSION} RUN mkdir -p /work /output /executions && chown node:node /work /output /executions USER node ENV HANKWEAVE_RUNTIME_EXECUTION_BASE_DIR=/executions WORKDIR /work ENTRYPOINT ["hankweave"] ``` Build it from the extracted bundle root: ```sh docker build -t hankweave-fixture:0.10.0 container-smoke ``` **Check it.** Run the image as its non-root user and require the command to print `0.10.0` and exit 0: ```sh docker run --rm --user node hankweave-fixture:0.10.0 --version ``` The matching capture records `0.10.0` followed by `exit=0`. If you override the image user to root, the Claude SDK refuses to start; use the image's `node` user instead of disabling that safety check. ## Build the contributor image from source When contributors need an image built from source, build the public repository `SouthBridgeAI/hankweave-runtime` at tag `v0.10.0`, commit `d0f0a86bcf4528f23ffa687b9c8964c15fe7cb88`. Use the repository's `bun.lock` lockfile. It does not use `bun.lockb`. ## Wire inputs and preserve state Whichever image you built, a run needs three things from outside the container: a hank, a data source, and provider keys. Mount the first two and pass the keys through environment variables; never bake secrets into the image or its build layers. ```sh # Keep provider keys outside the image. docker run --env-file .env hank.json data/ ``` Mount the data read-only. The runtime surfaces it inside the execution at `agentRoot/read_only_data_source` as a symlink by default, or as a copy when you pass `--copy`; see the [runbook](/0.10.0/files/operate/runbook) for the data-mount procedure. ![Container mount and environment wiring for Hankweave execution](/content-assets/cf45dff5691c48c0/diagrams/operate-deployment/1.png) Container mount and environment wiring for Hankweave execution
Diagram as text ```text +-- host ----------------------------+ +-- container -----------------+ | | | | | ANTHROPIC_API_KEY . . . . . . . . . env . . . . .> hankweave | | | | | -o /output | | minimal-single-provider/ ---------|-- mount :ro --> /work (ro) | | | | | | | container-executions/ ----------|-- mount :rw --> /executions | | | | v | | container-output/ <--------------|-- mount :rw --- /output | +------------------------------------+ +------------------------------+ ```
State needs the same treatment as input. Persist the execution directory with a bind mount or named volume, because it carries `.hankweave/` state, checkpoints, and logs. Use `-e ` to select a particular persisted execution directory so a later container can resume it. Without `-e`, new executions are created below `~/.hankweave-executions/`. The effective way to move that managed root is `HANKWEAVE_RUNTIME_EXECUTION_BASE_DIR`; `hankweave.json`'s `executionBaseDir` field is informational and does not relocate it. See the [hankweave.json reference](/0.10.0/files/reference/hankweave-json) for that key's contract. Output-copy name collisions get an `_N_timestamp` suffix by default; `--overwrite-output` overwrites instead. A mounted results volume receives copied output only when you pass `-o /output`; without `-o`, output remains in `agentRoot`. > **Pitfall:** Mounting a results volume without `-o` collects nothing. Set per-environment runtime overrides with `HANKWEAVE_RUNTIME_*`, including `PORT`, `MODEL`, `AUTOSTART`, and `SHOW_COSTS`, rather than editing `hank.json`. Use model shortcuts from the [model-resolution reference](/0.10.0/files/reference/model-resolution), not the retired model ID from the old container example. Sentinel provider keys use the `HANKWEAVE_SENTINEL_` prefix; the key and model setup belongs to [authentication and models](/0.10.0/files/operate/authentication-and-models). Hankweave excludes both `HANKWEAVE_RUNTIME_*` and `HANKWEAVE_SENTINEL_*` from the agent process environment, so runtime settings and sentinel keys do not leak into a codon (one sealed agent task). With inputs, state and secrets settled, we can run the complete non-root consumer recipe. Build from the bundle root and make the two writable bind mounts writable by the image's `node` user (UID/GID 1000 on Linux). Change ownership only on these newly created directories when necessary: ```sh mkdir -p container-output container-executions # On Linux, only when the bind mounts need it: sudo chown 1000:1000 container-output container-executions docker run --rm --user node -e ANTHROPIC_API_KEY \ -v "$PWD/minimal-single-provider:/work:ro" \ -v "$PWD/container-output:/output" \ -v "$PWD/container-executions:/executions" \ hankweave-fixture:0.10.0 hank.json data/ --headless --start-new -o /output ``` The fixture's non-root capture exits 0 and leaves `summary.md` on the mounted output volume. Running the same image as root instead exits 1 with `--dangerously-skip-permissions cannot be used with root/sudo privileges`; fix the user and mount ownership rather than disabling the check. **Check it.** A non-interactive (`--headless`) run with `-o /output` exits 0 and leaves `summary.md` on the mounted results volume. ## Reach the server outside the container A running container also starts Hankweave's WebSocket control server, and whether you expose it is a separate decision from running the workload. Leave its port at the default, `0`, when no outside client needs to reach the server. The operating system assigns a free port after launch, which lets concurrent instances share a host without choosing colliding ports. When a published port, reverse proxy, or `--attach` client must reach the server, pin it with `-p` / `--port ` and publish the same container port. Bind the host port to `127.0.0.1` where local access is sufficient, or to a trusted private network; require an external authenticated access control for remote use. This WebSocket control channel is unauthenticated, and `READ_ONLY` is not a security boundary: its command set still includes `server.shutdown` and `server.force_shutdown`. The pinned-port recipe adds the port flag and publishes it on the loopback interface only: ```sh docker run --rm --user node \ -p 127.0.0.1:8080:8080 \ -v "$PWD/minimal-single-provider:/work:ro" \ -v "$PWD/container-output:/output" \ -v "$PWD/container-executions:/executions" \ -e ANTHROPIC_API_KEY \ hankweave-fixture:0.10.0 hank.json data/ --headless --start-new --port 8080 -o /output ``` After the server binds, it writes the chosen port to `/.hankweave/runtime.lock`. `--attach --execution ` reads that file to discover the port and falls back to `7777` when discovery provides no port. Pin a port only when outside reachability requires it; otherwise retain port `0`. ## Keep CI from reusing completed runs CI adds one hazard on top of the container recipe: silent resume. Use `--headless` for CI/CD and scripts. Keep the working tree clean, pass explicit `hank.json` and `data/` arguments, and do not use a bare invocation: a bare invocation launches the wizard. Do not add `--autostart`; the headless contract belongs to the [runbook](/0.10.0/files/operate/runbook). Start each fresh CI run with `--start-new`. Reuse is keyed on the data signature, so an already-completed execution can resume silently, return `0`, and run nothing new. Pass `-o` with the results directory. Inside an image whose entrypoint is the installed `hankweave` bin, the fresh-run recipe is: ```sh hankweave hank.json data/ \ --headless \ --start-new \ -o /output ``` For a deliberate non-interactive resume after a hank edit, omit `--start-new` and add `-y`; a changed hank hash otherwise makes the resume cancel: ```sh hankweave hank.json data/ \ --headless \ -y \ -o /output ``` See the [CLI reference](/0.10.0/files/reference/cli) for flag defaults and deprecations. Gate the job on its process exit code: `0` means success and `1` means failure, including `outputFiles` copy and `beforeCopy` failures since 0.9.0 (see [exit codes and errors](/0.10.0/files/reference/errors-and-exit-codes)). A 30-second shutdown watchdog force-exits a wedged graceful shutdown. `--max-cost` and `--max-time` are parsed and validated as run-level caps, but are absent from shipped `--help`. > **VersionNote:** Since 0.9.0, an `outputFiles` copy or `beforeCopy` failure returns exit code `1`, so gate CI on that result. Headless stderr can contain PostHog `ENOTFOUND hw-telemetry.southbridge.ai` stack traces when the telemetry host cannot be reached. In 0.10.0 this known telemetry defect is log noise, not a run failure. **Check it.** Require a failing hank to return `1`, and a completed job to return `0` with results present. The captured completion observable is: ```jsonl {"id": "", "timestamp": "", "type": "codon.completed", "data": {"codonId": "summarize-notes", "success": true, "cost":"", "duration":"", "exitStatus": {"type": "success"}}} {"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "RunCompleted", "runId": "", "transition": {"type": "RunCompleted", "data": {"runId": ""}}, "resultingState": {"currentRunId": null, "runCount": 1, "totalCost":"", "currentRunCost":""}}} ``` The container capture also shows that a rerun without `--start-new` resumes the completed execution, exits 0, and does no new work. Do not treat that successful exit as proof that a fresh run occurred. ## Schedule batch runs on Kubernetes The same batch behavior maps onto Kubernetes with a few platform-specific adjustments. Use a `Job` with `restartPolicy: Never` and a bounded `backoffLimit` for batch runs. See [Deployment model](/0.10.0/files/integrate/deployment-model) for ports, locks, and exit-code behavior; adapt the volume and secret configuration for your deployment. Pass flag values with spaces. The equals-sign form is deprecated everywhere, not only in Kubernetes: ```sh --port 8080 --execution /executions/job-001 ``` Do not carry `--flag=value` into new manifests. The `--proxy` flag is a separate HTTP proxy for Claude API requests; it is disabled by default and opts in with `--proxy`. It is not an authentication layer for the WebSocket control channel. ## Mirror the package for air-gapped installs Air-gapped environments need both halves of the deployment mirrored: the versioned npm tarball and the public repository tag together. The 0.10.0 tarball contains `dist/` and `schemas/` (the package's schema files), but no `CHANGELOG`; mirroring the tarball and the `v0.10.0` repository tag carries what an air-gapped install needs. For an Anthropic endpoint behind a corporate proxy, pass the documented base-URL option: ```sh hankweave hank.json data/ --anthropic-base-url ``` The equivalent `hankweave.json` key is `anthropicBaseUrl`. Do not use the old Ollama recipe for 0.10.0: the runtime does not read `OLLAMA_HOST`. ## Check liveness without a `/health` endpoint The last piece of a deployment is knowing whether the running container is healthy. Do not probe the main server at HTTP `/health`: it exposes no such route in 0.10.0. The only `/health` route belongs to the LLM proxy, which is disabled by default and enabled with `--proxy`. Instead, the runtime writes `/.hankweave/runtime.lock` as JSON and refreshes `lastHeartbeat` every 30 seconds. The lock record carries the PID, run ID and heartbeat you need: ```typescript // Update lock file with runId and heartbeat interface LockFile { pid: number; runId: string; startTime: string; lastHeartbeat: string; port?: number; // Optional for backward compatibility with old lock files } const lockData: LockFile = { pid: process.pid, runId, startTime: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), port: this.config.port, // NOTE: May be 0 initially if using dynamic port; updated after server binds ``` Use both signals in a container healthcheck: the PID must be alive and `lastHeartbeat` must be no more than 120 seconds old. On its next start, Hankweave removes a lock with a dead PID or a heartbeat older than 120 seconds and marks the run crashed. Do not replace that test with `test -f .../runtime.lock`; file existence can pass for a wedged server whose heartbeat is stale. **Check it.** During a run, verify that the mounted lock's PID is alive and its `lastHeartbeat` stays fresh at roughly the 30-second update cadence plus scheduling jitter.