Execution directory

Every hank run writes its work into a single execution directory: the agent workspace (agentRoot/), the archive of files moved out by archiveOnSuccess (rigArchive/), and the runtime's own bookkeeping (.hankweave/). Because all three live together, a completed execution is self-contained: we can inspect it, resume it, replay it, or pack it up as a debugging snapshot.

This page is the lookup reference for that directory. It covers where the directory is created and how Hankweave protects explicit paths, what each top-level entry contains, the state and log files under .hankweave/, the archive layout, replay behavior, checkpoint file watching, and the lifecycle rules that apply at cleanup time.

Where the execution directory lives#

By default, Hankweave creates execution directories under the managed root ~/.hankweave-executions/. Set HANKWEAVE_RUNTIME_EXECUTION_BASE_DIR to move that root; hankweave.json's executionBaseDir is informational only and does not move it. getManagedExecutionsRoot() reads the environment variable at call time when the managed root is requested.

When Hankweave auto-creates a directory under the managed root, the name follows this shape:

Output
{timestamp}-{random}-{dataHashFirst6}

Here, timestamp is Date.now(), random is the four-character result of Math.random().toString(36).substring(2,6), and dataHashFirst6 is the first six characters of the 12-character hex SHA-256 digest produced for the data source. For a directory, that digest covers a bounded manifest of entry names, types, sizes, and modification times; the scan reaches depth 3, considers at most 100 entries per directory, and can be truncated. For a single file, its metadata and contents are hashed. The hash in the name is what makes resume work: on a managed rerun (no --execution), Hankweave finds the most recent existing directory with the same dataHash, and creates a new one only when there is no match.

To choose the location yourself, pass --execution (or -e). Hankweave creates an absent path. What happens next depends on what is already there and on --start-new. Tier 1 of the safety checks below applies to every explicit path; the Tier 2 and Tier 3 checks run only when --start-new is also supplied. Without --start-new, an existing directory without .hankweave/ is adopted after printing Using existing directory as execution directory: <path>, with no prompt or counts. An existing execution with .hankweave/execution-meta.json is resumed after its data-hash check, and only on that explicit path does a changed hank.json matter: interactively it warns and asks Continue with modified config?, and non-interactively it fails closed unless -y or --force is used. A managed resume never reads the hank hash; it reuses the persisted plan.

The managed root and an explicitly selected execution are separate concepts: set the environment variable for the former, and use --execution for the latter. Either way, a hank run uses the selected execution directory for its workspace and state.

⌁ Terminal
HANKWEAVE_RUNTIME_EXECUTION_BASE_DIR=/var/lib/hankweave-executions \
  bunx hankweave@0.11.0 ./hank.json ./data

Tiered safety for execution directories#

Because an explicit path can point anywhere, Hankweave applies three protections to it. Tier 1 is unconditional; Tier 2 and Tier 3 are part of the --start-new branch:

  1. Managed-root protection. A new --execution path inside ~/.hankweave-executions/ is rejected. An existing execution there may be resumed; manually creating a new one there through --execution is not allowed.
  2. Existing execution metadata. With --start-new, a path that already contains .hankweave/ refuses a fresh start unless --force is also supplied. That operation renames .hankweave/ to .hankweave.backup-{timestamp} and wipes agentRoot/, unless --no-wipe preserves it.
  3. Non-empty path without metadata. With --start-new, a non-empty directory without .hankweave/ asks for confirmation and reports file and directory counts before agents receive read/write access. --force or -y skips that prompt.

--start-new/-n requests a fresh execution. Without --execution, the fresh directory is auto-created under the managed root; with an explicit path, the protections above still apply.

One further guard applies only on an explicit resume. When -e <dir> names a directory that already holds .hankweave/execution-meta.json and the data-source hash differs from the recorded one, the resume is blocked with the expected and current hashes; --force or --ignore-data-mismatch overrides that mismatch. A managed rerun (no --execution) instead looks executions up by the current data hash: when none matches, it creates a new directory and runs every codon from the start, with no error.

The directories you see at the top level#

Setup creates three principal entries:

FIG. 1 The directories you see at the top level
Read the diagram as text
Output
{execution}/
├── agentRoot/
├── rigArchive/
└── .hankweave/

agentRoot/ is the workspace passed to agent processes. Agent file operations occur there, and files matching checkpointedFiles patterns are candidates for checkpoint (a Git snapshot) tracking. rigArchive/ starts empty and stores files moved by archiveOnSuccess; its layout is described below. .hankweave/ holds everything the runtime writes about the run, detailed in the next two sections. A completed root can also contain the checkpoint .gitignore and model-validation.log, which records model-catalog validation logging during startup.

To make that concrete, here is the observable layout of a completed one-codon run (one agent task). The capture was taken after --restore-journal rebuilt events.jsonl beside the as-left dieted pair, so the events directory shows the restored form, not what the run left on disk:

Output
.
├── .gitignore
├── .hankweave
│   ├── archive-manifest.json
│   ├── checkpoints
│   │   ├── .gitconfig
│   │   └── .hankweavecheckpoints
│   │       ├── COMMIT_EDITMSG
│   │       ├── HEAD
│   │       ├── ORIG_HEAD
│   │       ├── config
│   │       ├── description
│   │       ├── hooks
│   │       ├── index
│   │       ├── info
│   │       ├── logs
│   │       ├── objects
│   │       └── refs
│   ├── events
│   │   ├── events.jsonl
│   │   ├── events.jsonl.zst
│   │   └── journal.meta.json
│   ├── execution-meta.json
│   ├── logs
│   │   ├── claude-sessions
│   │   │   └── eb6315c7-b196-4afa-b9e6-e68b117a1988.jsonl
│   │   └── server.log
│   ├── runs
│   │   └── <id>
│   │       └── write-line-claude.log
│   ├── sentinels
│   │   └── history
│   ├── state.json
│   └── state.json.bak
├── agentRoot
│   ├── out.txt
│   └── read_only_data_source -> <workspace>/fixtures/scenarios/execution-directory/data
├── model-validation.log
└── rigArchive

Note read_only_data_source/ inside agentRoot/. By default it is a symlink to the original data source; --copy requests a copy instead. If the symlink system call fails, Hankweave falls back to a directory copy. For a single-file source, the file is placed at read_only_data_source/{basename}. Nothing makes this directory read-only: the name is a convention, and a write through the symlink lands in the original data directory. Checkpoints always exclude the path. Designing codons and handoffs owns the detail.

The --init scaffold also supplies these repository ignore rules, so in a project created with --init the checkpoint store at .hankweave/checkpoints/.hankweavecheckpoints sits inside the scaffold's ignored .hankweave/ directory:

Output
.hankweave/
*.log
node_modules/

Its README documents adding rigSetup to prepare an environment before a codon runs:

Output
…
- Add `rigSetup` to prepare your environment before a codon runs
…

The files Hankweave writes to track execution#

The entries under .hankweave/ fall into two groups: small files that identify the execution and its owning process, and larger stores that persist history. This section covers the identity files and the two metadata files that sit beside them.

.hankweave/runtime.lock#

runtime.lock contains the process and run identity used for crash detection, successor detection, and attach-port discovery. Its fields are pid, lockId (a token unique to each lock acquisition, which teardown and the heartbeat use to tell their own lock from a successor's), runId, startTime, lastHeartbeat, and port; the heartbeat is refreshed every 30 seconds.

TYPESCRIPT
    interface LockFile {
      pid: number;
      /**
       * Unique per lock acquisition. Pid + "acquired once" is not identity:
       * after >120s of missed heartbeats a successor may legitimately
       * replace this lock, and teardown/heartbeat must then recognize the
       * file is no longer theirs instead of unlinking or refreshing it.
       */
      lockId: string;
      runId: string;
      startTime: string;
      lastHeartbeat: string;
      port?: number; // Optional for backward compatibility with old lock files

.hankweave/execution-meta.json#

execution-meta.json records the execution identity and environment. It carries a format version (shown in the construction below) and includes hankweaveVersion, an environment object with invocationMethod, platform, arch, osRelease, and runtime, and the identity/time fields dataHash, hankHash, hankPath, linkType, createdAt, and lastUsed. It also records readOnlySourceDataPath as supplied and readOnlySourceResolvedDataPath as the real path returned by fs.promises.realpath.

The following is the TypeScript construction that writes the metadata, not a captured JSON record:

TYPESCRIPT
  const meta = {
    version: "1.1.0",
    readOnlySourceDataPath,
    readOnlySourceResolvedDataPath: await fs.promises.realpath(readOnlySourceDataPath),
    dataHash,
    hankHash,
    hankPath,
    linkType,
    createdAt: isNewExecution
      ? new Date().toISOString()
      : (existingMeta?.createdAt ?? new Date().toISOString()),
    lastUsed: new Date().toISOString(),
    hankweaveVersion: getMetadata().version,
    environment: {
      invocationMethod,
      platform: process.platform,
      arch: process.arch,
      osRelease: os.release(),
      runtime: getRuntimeVersion(),
    },
  };

State and archive metadata#

.hankweave/state.json is the primary persistence file for the complete execution history: runs, codons, checkpoints, and costs. state.json.bak is written before each save; if the primary file is corrupted, Hankweave falls back to the backup. The complete schema is in State file reference.

.hankweave/archive-manifest.json tracks files archived by archiveOnSuccess. It sits outside the checkpointed Git work tree, carries a format version, and starts with an empty entries array:

JSON
{ "version": "1.0.0", "entries": [] }

The manifest is not checkpointed by git. Rollback selects the entries to restore by git reachability rather than manifest list order and removes exactly the restored ones; during restoration the target checkpoint tree is authoritative, so a destination the target tree already owns is skipped with its archive copy and manifest entry kept, and a failing entry is recorded with its error and kept (the caller removes only the successful entries). The exact rollback contract is in Resume, rollback, and retry; the archive paths and entry fields are described in How rigArchive/ is organized.

The subdirectories and their contents#

Below the metadata files, .hankweave/ grows subdirectories as the run proceeds: an event journal, logs, per-run agent transcripts, the checkpoint Git store, and sentinel state.

Events and logs#

While a run is live, .hankweave/events/events.jsonl is the append-only JSONL event journal: every journaled server event is one JSON object per line, and line order supplies the audit trail. An advisory sidecar, events.meta.json (carrying schemaVersion, eventSchemaVersion, totalEvents, byteLength, and lastEventId), is written after a startup rescan, every 1,000 appended events, and when the journal closes. See Events for the event schema and The event journal for the diet contract.

A finished run may not leave that plain journal. At shutdown, finalization diets the journal by default when the runtime can still prove it owns runtime.lock and the run ended completed or failed: large inline bodies move to events/cas/<sha-prefix>/<sha>.zst, the rest compresses whole to events.jsonl.zst, and journal.meta.json (which carries eventSchemaVersion after the diet) supersedes the sidecar. A skipped or failed diet leaves the plain journal and sidecar in place. To read a dieted journal, restore it first with bunx hankweave@0.11.0 --restore-journal <exec>, or read it through the runtime's own journal reader, which serves events.jsonl.zst transparently.

The logs/ directory is created lazily on the first logger write. The runtime writes logs/server.log there: timestamped plain text for startup, codon transitions, and debug output. It also stores harness session directories such as claude-sessions/ and pi-sessions/. The runtime config declares socketLogFile with the default .hankweave/logs/websocket.log, but no runtime code resolves that field or calls its only writer, so no run writes that file. The table below is the declared entry shape of that unwritten path, not the format of a file a run leaves; the record of broadcast traffic is the event journal, which excludes single-client events and client commands:

Scroll to explore the table →
WebSocket fieldMeaning
loggedAtWhen the message was logged
directionin for client-to-server; out for server-to-client
messageThe complete ClientCommand or ServerEvent
metadata.sizeMessage size

Configured sentinels also store output and conversational history below .hankweave/sentinels/.

Run folders and agent logs#

.hankweave/runs/{runId}/ is created when a run starts. Agent logs there are JSONL with message types system, assistant, user, and result; assistant carries usage, and result carries total_cost_usd. State stores the agent-log path as claudeLogPath, relative to executionPath, for example .hankweave/runs/{runId}/observe-0-claude.log.

Checkpoint storage#

.hankweave/checkpoints/ contains .hankweavecheckpoints/, the Git repository that tracks selected files in agentRoot/, and .gitconfig, the isolated configuration with user.name = Hankweave Runtime and gpgsign = false. The store is named .hankweavecheckpoints rather than .git so an execution directory is not detected as a Git submodule. Git operations point HOME and XDG_CONFIG_HOME at this checkpoint directory instead of using the operator's global configuration. See Checkpoints for store mechanics.

When inspecting from agentRoot, select the checkpoint repository and work tree explicitly:

⌁ Terminal
git --git-dir=../.hankweave/checkpoints/.hankweavecheckpoints \
  --work-tree=. log --oneline

Sentinel outputs and history#

Sentinel outputs are under .hankweave/sentinels/outputs/{sentinelId}/ and use {id}-{codonId}-{timestamp}.{ext}. The extension is md for text, ndjson for structured output with structuredOutput: true, or jsonl when output.format is "jsonl". Conversational state is under .hankweave/sentinels/history/{sentinelId}-codon-{codonId}.json; it stores turn history, token counts, and trim metadata between triggers. Output-path resolution belongs to Sentinel configuration. The package's public exports are only ., ./schemas, and ./types; the export contract belongs to Client and exported types.

How rigArchive/ is organized#

archiveOnSuccess moves file-glob matches from agentRoot/ to rigArchive/ when the owning codon completes: each match is copied to the archive and then removed from the workspace, so later codons and iterations no longer see it. Its entries are file globs (file patterns), so a tree uses a pattern such as current-project/**, not a bare directory.

Where a file lands under rigArchive/ depends on whether its codon ran inside a loop:

Scroll to explore the table →
Archive casePath relative to rigArchive/Timing and example
Non-loop codon{codonId}/{sourcePath}edit/current-project/history.txt
Codon inside a loop iteration{loopId}-{iteration}/{runtime-codon-id}/{sourcePath}revise-0/edit-0/current-project/history.txt
Loop-level archive{loopId}-loop/{sourcePath}revise-loop/current-project/history.txt

For a loop codon, runtime-codon-id is the codon's runtime ID ({codonId}#{iteration}, for example edit#0) with # replaced by -, so iteration 0 of edit appears in the archive path as edit-0. Loop IDs do not contain #. A loop-level archive runs whenever a completed or skipped loop codon does not add codons to the plan: at the loop's real termination, and also after every codon of an iteration except its last. It runs once only when the loop body has a single codon; in a multi-codon body, later codons of the iteration find the archived paths already gone. The source path is relative to agentRoot/; from an agentRoot command, the sibling archive begins at ../rigArchive/.

The loop fixture uses the file glob explicitly:

JSON
"archiveOnSuccess": ["current-project/**"]

Its observed first-iteration file is ../rigArchive/revise-0/edit-0/current-project/history.txt when addressed from agentRoot, matching the loop-iteration row of the table above.

Each manifest entry records sourcePath relative to agentRoot/, archivePath relative to the execution root, codonId, optional loopContext (loopId and iteration), checkpointSha, and timestamp. Rigs, Codons, and Loops link here for this path shape.

When --replay copies the directory#

--replay copies the source execution to $TMPDIR/hankweave-replay-{Date.now()}-{random}/ before starting. The original remains available as a read-only artifact. Hankweave removes runtime.lock from the temporary copy so a live source run does not block replay startup, then removes the temporary directory when the process exits.

Use --replay with the source execution as its argument. When no hank or data paths are supplied, the CLI discovers hankPath and readOnlySourceDataPath from .hankweave/execution-meta.json, making --replay <dir> self-contained. Replay cannot be combined with --execution; supplying both flags exits with an error.

Replay always starts a fresh run, even when the source is complete, and replays all codons from scratch. Each codon's harness is replaced by a replay process manager that re-emits the recorded per-codon JSONL log, so no codon makes a new model call and the recorded codon output is reproduced. Replay also skips the per-model startup self-tests, including the Pi model_catalog preflight, and skips rig setup and sentinels (the copied directory already holds the post-setup state), so it needs no harness credentials and does not prove that changed rigs or external side effects work. A normal (non-replay) startup and --validate run those self-tests.

Two limits remain. Replay does not skip the runtime's startup provider health checks: the sentinel manager is constructed regardless and starts health checks on its own provider registry, which can make billable requests for every provider whose credentials are set. Replay also copies into the system temporary directory and sends telemetry unless disabled, so it is neither hermetic nor provider-free. In the pinned completed-execution capture, the listener initialized, the process exited 0 on its own, and the original state and event journal remained byte-identical. That is one capture, not a promise that every replay stays open or exits immediately, nor an absolute offline or zero-bill guarantee.

⌁ Terminal
bunx hankweave@0.11.0 --replay ./my-execution

How checkpoint file watching works#

File-change detection is scan-on-demand. After a file tool call that could modify the filesystem, such as Write or Edit, the runtime feeds the tool call to the codon's file tracker, whose synchronous path matcher decides whether the path is watched; it does not maintain a persistent filesystem watcher.

checkpointedFiles patterns accumulate across codons. After codon 1 declares *.ts and codon 2 declares *.md, later checkpoints continue to use both patterns. Before a checkpoint, the runtime re-adds the patterns from every codon through the current one into a trackedPatterns set, which is not cleared between codons.

UnifiedFileResolver combines .gitignore rules from the root and every subdirectory, rewriting subdirectory-relative rules to project-relative paths and caching the combined rules per project path. Before parsing those files, it always ignores .git/, read_only_data_source/, .hankweavecheckpoints/, -quarantine-*, and .hankweave.backup-*. Those hard-coded exclusions apply regardless of checkpointedFiles.

Lifecycle of the execution directory#

--cleanup removes execution artifacts; CLI reference owns its exact scope. The directory is not automatically cleaned up after a run, so it remains available for inspection, resume, or replay.

For a debugging snapshot, archive .hankweave/, agentRoot/, and rigArchive/ together. Those three parts reconstruct the execution state; read_only_data_source/ remains a symlink by default or is copied when --copy was used.

Two guards apply only when an existing directory is passed to --execution without --start-new: a path containing both /.hankweave-executions/ and /data is refused with Cannot create execution inside another execution directory, and a path equal to the resolved data source is refused with Execution directory cannot be the same as data source. With --start-new, a non-empty data directory instead gets the Tier 3 warning and prompt, and -y or --force adopts it. Keep the source and execution directory distinct, and choose another explicit execution path or omit --execution when one of these checks applies.