You're reading the 0.10.0 archive.

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 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, 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. Export ANTHROPIC_API_KEY before a run; do not put provider keys in the image.

⌁ Terminal
# 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:

⌁ Terminal
npm install -g hankweave@0.10.0

With Bun, pin the version on each invocation instead:

⌁ Terminal
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:

⌁ Terminal
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:

⌁ Terminal
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.

⌁ Terminal
# Keep provider keys outside the image.
docker run --env-file .env <image> 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 for the data-mount procedure.

FIG. 1 Container mount and environment wiring for Hankweave execution
Read the diagram as text
Output
+-- 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 <path> 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 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.

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, not the retired model ID from the old container example. Sentinel provider keys use the HANKWEAVE_SENTINEL_<VAR> prefix; the key and model setup belongs to 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:

⌁ Terminal
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 <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:

⌁ Terminal
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 <executionPath>/.hankweave/runtime.lock. --attach --execution <dir> 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.

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:

⌁ Terminal
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:

⌁ Terminal
hankweave hank.json data/ \
  --headless \
  -y \
  -o /output

See the CLI reference 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). 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.

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": "<id>", "timestamp": "<ts>", "type": "codon.completed", "data": {"codonId": "summarize-notes", "success": true, "cost":"<n>", "duration":"<n>", "exitStatus": {"type": "success"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "RunCompleted", "runId": "<id>", "transition": {"type": "RunCompleted", "data": {"runId": "<id>"}}, "resultingState": {"currentRunId": null, "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}

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 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:

⌁ Terminal
--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:

⌁ Terminal
hankweave hank.json data/ --anthropic-base-url <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 <executionPath>/.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.