Skip to content

harn artifacts

harn artifacts <subcommand> [options]

Working artifacts sit between the journal and tracked project files. Use them for screenshots, exports, browser captures, audit dumps, experiments, and rollback inputs that need to survive a session but should expire later. Each workspace lives under .harnery/artifacts/ with a versioned manifest.

Terminal window
harn artifacts create <slug> --purpose <text> [--days <n> | --minutes <n>] [--big [--allow-low-disk]]
harn artifacts adopt-unmanaged [--days <n>] [--big] [--yes]
harn artifacts list
harn artifacts show <ref>
harn artifacts delivery-card <ref> [--title <text>] [--url <label=url>...] [--path <label=path>...]
harn artifacts renew <ref> (--days <n> | --minutes <n>) --reason <text>
harn artifacts release <ref>
harn artifacts discard <ref> --reason <text> [--minutes <n>]
harn artifacts remove <ref> --reason <text> [--yes]
harn artifacts capabilities --json
harn artifacts create <slug> --purpose <text> --hold <id> --hold-reason <text> --actor <instance-id>
harn artifacts hold <ref> --id <id> --reason <text> [--days <n> | --minutes <n>] [--actor <instance-id>]
harn artifacts unhold <ref> --id <id> [--actor <instance-id>]
harn artifacts migrate [--yes]
harn artifacts repair-activity [--yes]
harn artifacts clean [--yes]

<ref> can be an artifact ID, a direct-child directory name, or a path to one direct child of .harnery/artifacts/.

Terminal window
# Create a workspace. The result includes an absolute path and artifact ID.
harn artifacts create checkout-audit \
--purpose "Compare checkout screenshots across viewports"
# Write screenshots and reports under the returned path.
# Stop treating the creating agent's heartbeat as active-use protection.
# Keep the normal window when review, a handoff, or final evidence needs it.
harn artifacts release <artifact-id>
# For reviewed disposable captures, use discard instead of release.
harn artifacts discard <artifact-id> --reason "Reviewed; superseded by the final capture"
# Preview cleanup. This never changes the filesystem.
harn artifacts clean
# Delete only entries still classified managed-expired after a final recheck.
harn artifacts clean --yes

After inspecting temporary captures, discard them when they are disposable or superseded and no unresolved failure, handoff, final evidence, or user review depends on them:

Terminal window
harn artifacts discard <artifact-id> --reason "Reviewed; newer run contains the final evidence"

This records the reason, releases active-owner protection, and shortens the rolling retention window to at most 60 minutes. It deletes nothing immediately. Use --minutes 15 for a shorter grace period. An earlier deadline stays earlier; repeating the command never extends it. An already-expired artifact stays expired. New payload activity restarts the shortened window.

The command refuses held artifacts, another live agent’s artifacts, tracked content, symlinks, and unreadable state. Use a hold for files awaiting review. A passing check does not establish whether someone still needs its evidence; the agent reviewing the result makes that decision. Keep the ordinary three-day window when unsure. release alone continues to preserve retention.

For work known to need a short window from creation:

Terminal window
harn artifacts create layout-probe --purpose "Temporary layout check" --minutes 90
harn artifacts renew <artifact-id> --minutes 180 --reason "Still reviewing"

create and renew accept whole minutes from 1 through 5,256,000, or whole days from 1 through 3,650. The flags are mutually exclusive. renew requires one of them and can restore a longer window before cleanup deletes the files. Artifact creation and release, managed qa-run results, and terminal qa-status reports include after_review guidance with the appropriate discard command. Release returns this reminder alongside its unchanged manifest fields. The reminder never shortens retention by itself.

When reviewed files are no longer needed, the creating agent can preview removal of one workspace, then delete it immediately:

Terminal window
harn artifacts remove <artifact-id> --reason "Reviewed; no remaining use"
harn artifacts remove <artifact-id> --reason "Reviewed; no remaining use" --yes

Preview leaves the payload and manifest unchanged. --yes removes the whole folder without waiting for expiry, including when its owner is active or its retention window has time left. Stop any writer first. The command requires a current agent identity matching the manifest’s creating owner, and refuses holds in force, peer file claims, tracked content, invalid manifests, symlinks, mount boundaries, special files, embedded repositories or artifact stores, and the caller’s working directory or its ancestors. It shares the artifact mutation lock and rechecks the folder before deletion. There is no force or ownership override.

The result includes deleted, the workspace entry, and the supplied reason. Applied removals append the reason and removed_by actor to the existing deletion log on a best-effort basis. artifacts list includes recent removals, and show explains why a removed workspace is gone when its record is available. The owner_scoped_remove capability advertises this command.

Use discard when a grace period is useful, or release to keep the existing retention window. clean --yes still applies the repository cleanup policy.

Create a compact Markdown card with no setup:

Terminal window
harn artifacts delivery-card <artifact-id>

The card links the artifact folder and up to five visible root-level directories and regular files. Directories appear first, then files, with each group sorted by name. Harnery skips its own metadata, hidden entries, symlinks, and special files. When more entries exist, the card reports the omitted count and points to the artifact folder instead of expanding the handoff.

Artifact links use the configured Harnery dashboard. When a live tunnel fronts that dashboard port, the card uses its public URL for every artifact file and folder. Otherwise, it uses the local dashboard URL. A stale tunnel or a live tunnel for another service is ignored.

Add a manifest when the card needs a remote URL, a friendly label, or an important file below the root:

Terminal window
harn artifacts delivery-card <artifact-id> \
--title "Video review" \
--url "Video=https://media.example/render.mp4" \
--path "Motion map=finished/motion-map.png" \
--path "Frames=finished/frames" \
--path "Report=finished/report.json"

Supplying items validates every local destination, saves .harnery-delivery.json in the workspace, and writes paste-ready Markdown to standard output. A saved manifest is an allowlist: the card includes those destinations and does not append unrelated root entries. The card contains a linked list followed by a fenced plain-text block. The list uses short labels so it stays easy to scan. The block contains every complete URL or filesystem path for copying. The artifact folder is always included automatically.

Run the command again without item options to reproduce the saved selections:

Terminal window
harn artifacts delivery-card <artifact-id>

The manifest is optional. --url and --path are repeatable. URL entries must use HTTP or HTTPS. Path entries must be relative to the artifact workspace, must already exist, and cannot escape through .. or a symlink. Supplying new items replaces the saved delivery manifest. Supplying only --title changes the heading and preserves the existing destinations.

On WSL, the plain-text block renders local destinations as Windows UNC paths so they remain directly copyable. Other systems receive their native absolute paths. A delivery manifest records navigation, not retention; move anything that must outlive the workspace into durable project storage.

New workspaces expire after three inactive days by default. Creating, editing, renaming, or deleting payload files restarts that window. Harnery derives activity from filesystem timestamps when it inventories or cleans the workspace, so agents do not need to run a separate touch command. Internal manifest writes (migration, release, hold, and unhold) preserve the activity clock. Explicit renewal still starts the requested retention window.

The manifest records an activity checkpoint and a fingerprint of root entry names. This separates the parent directory changes caused by manifest replacement from payload deletions and renames. Nested directories and files keep their ordinary timestamps. Invalid checkpoints protect the workspace from cleanup. Manifests without a checkpoint retain their conservative filesystem-based clock until a normal metadata mutation records one or a verified repair applies.

Set a project default in .harnery/config.jsonc:

{
"artifacts": {
"default_retention_days": 7
}
}

HARNERY_ARTIFACT_RETENTION_DAYS overrides the config value for one process. create --days or create --minutes overrides both. renew starts a new rolling window of the requested length. The expires_at field returned by list and show is the effective deadline after accounting for the latest change.

Managed artifacts have two byte limits by default:

  • The whole repository has a soft 20 GiB budget. When unheld managed bytes exceed it, Harnery selects idle workspaces by earliest effective expiry until the remainder fits. Held workspaces do not count toward the budget, because cleanup can never remove them.
  • One workspace has a 1 GiB ceiling. Pass create --big when a job is expected to exceed it, or run artifacts allow-big <ref> when an existing workspace turns out larger than planned. An idle oversized workspace without that acknowledgement becomes a cleanup candidate even before its time window expires. Unlike a hold, the acknowledgement leaves expiry in force.

Both limits measure disk use: allocated blocks, with each hard-linked file counted once. A sparse disk image counts only the blocks it has written. list reports this as bytes and the plain sum of file lengths as apparent_bytes; a large gap between them means sparse or hard-linked files.

Neither limit deletes a workspace until it has been idle for 24 hours. Idle means no change inside the workspace, no renewal or release, and no heartbeat from its owner. An agent waiting for a human reply stops sending heartbeats, so a shorter window would delete work someone is still reviewing. Expiry does not wait for this grace. While a workspace is over the ceiling but not yet eligible, list and show carry a warning that says when cleanup will remove it and how to keep it. create without --big returns the ceiling as size_ceiling_bytes with a size_note.

Holds in force, live owners, Git-tracked content, unreadable paths, and direct-child symlinks stay protected under pressure. The limits therefore bound eligible managed data, not storage Harnery cannot safely delete. Configure the limits in bytes and the grace in hours:

{
"artifacts": {
"max_bytes": 21474836480,
"max_unit_bytes": 1073741824,
"idle_grace_hours": 24
}
}

HARNERY_ARTIFACT_MAX_BYTES, HARNERY_ARTIFACT_MAX_UNIT_BYTES, and HARNERY_ARTIFACT_IDLE_GRACE_HOURS override them for one process.

list and clean expose managed-over-budget and managed-oversize before anything changes. clean --yes recalculates the whole plan and checks the candidate’s ID, size, last modification, and expiry immediately before removal.

Held work never counts toward the repository budget and is never deleted by a size rule, so it has its own advisory budget, artifacts.max_held_bytes (default 20 GiB). When held workspaces use more, list and clean report it and keep everything. Their meta carries:

  • held_bytes, max_held_bytes, and held_over_budget.
  • largest_holds: the five largest held workspaces, each with its name, bytes, and holds (id, set_by, expires_at, persistent).
  • free_bytes on the artifact store’s filesystem (null when it cannot be read), min_free_bytes, and low_disk.
  • warnings: plain sentences naming what to release or prune. The count of rows that carry their own warning is row_warnings.

The same warnings print on standard error, so JSON on standard output stays parseable. When free disk drops below artifacts.min_free_bytes (default 20 GiB; 0 disables the floor), artifact commands warn and create --big refuses:

Terminal window
harn artifacts create renders --purpose "Full-resolution renders" --big
# refused: names free space, the floor, and the commands that free space
harn artifacts create renders --purpose "Full-resolution renders" --big --allow-low-disk

--allow-low-disk is a deliberate override for one command. A workspace created without --big only warns.

Every full inventory (list, clean, and the automatic sweep) records its measurement in .harnery/artifact-usage.json. create and hold never walk the store; they return usage (held_bytes and held_measured_at from that record, free_bytes measured now) and warnings. Configure the budget and floor in bytes:

{
"artifacts": {
"max_held_bytes": 21474836480,
"min_free_bytes": 21474836480
}
}

HARNERY_ARTIFACT_MAX_HELD_BYTES and HARNERY_ARTIFACT_MIN_FREE_BYTES override them for one process.

Older integrations sometimes wrote regular files directly under .harnery/artifacts/. Preview their adoption with:

Terminal window
harn artifacts adopt-unmanaged
harn artifacts adopt-unmanaged --yes

The command moves unchanged, untracked direct-child regular files into one managed workspace. It adopts an untracked legacy directory in place by adding its manifest, so existing paths keep working. Symlinks, tracked paths, invalid manifests, and entries that changed during the preview stay untouched. If the combined file workspace or one adopted directory exceeds the per-bundle ceiling, the mutating command also requires --big.

Session starts, artifacts create, and new managed QA workspaces offer cleanup opportunities. A completed sweep suppresses another for one hour. Automatic sweeps attempt at most ten deletions and yield between candidates after five seconds. The initial inventory and one filesystem operation can exceed that time budget; this is not a hard process timeout. Each slice attempts at least one candidate so a large store can still make progress. Partial or failed slices can retry on another opportunity after one minute.

The stamp file (.harnery/artifacts-auto-clean.json) distinguishes running, partial, failed, and completed attempts and records counts, bytes, remaining candidates, and failures. An interrupted attempt does not count as completion. The sweep deletes only entries still classified managed-expired, managed-oversize, or managed-over-budget after the final recheck, and never touches unmanaged or legacy directories. Every deletion, automatic or from clean --yes, appends a line to .harnery/artifact-deletions.jsonl with the path, artifact ID, owner, classification, reason, bytes, and time. The log keeps 30 days. list returns the last seven days as recent_deletions, and show on a deleted workspace names the rule that removed it. The mutation lock serializes the sweep and its timestamp with other artifact mutations. Lock contention does not consume the cleanup opportunity. Expiry and per-unit size checks rescan only the candidate; repository-budget eviction still refreshes the whole store because its decision depends on other entries.

Expiration makes files eligible for the next cleanup opportunity; it is not a background timer. An idle project keeps expired files until another session, new work, or an explicit cleanup. The independent event-ledger sweep stays daily.

Disable it per project or per process:

{
"artifacts": {
"auto_clean": false
}
}

HARNERY_ARTIFACT_AUTO_CLEAN=0 disables it for one process.

The separate review_pack.auto_clean setting defaults to false. When enabled, artifacts clean also previews expired managed review packs inside the artifact store. --yes removes their payloads and leaves an expiry summary. Holds and invalid workspace manifests still protect those packs; unexpired and unmanaged packs remain untouched. This setting also enables the review-pack sweep before qa-run and review-pack create.

Classification Meaning clean --yes
managed-held At least one hold is in force, regardless of expiry, owner activity, release, or size. Keep
managed-active The creating agent has a fresh heartbeat and has not released the unit. Keep
managed-current Retention has not expired. Keep
managed-expired Retention expired and no protection remains. Delete
managed-oversize A unit idle past the grace exceeds the per-bundle ceiling without --big. Delete
managed-over-budget Unheld managed disk use exceeds the repository budget and this unit, idle past the grace, expires earliest. Delete
managed-tracked Git tracks at least one path inside the unit. Keep
invalid-manifest The manifest is unreadable, invalid, or uses an unknown schema. Keep
unmanaged The entry has no managed manifest or is not a directory. Keep
symlink The direct child is a symlink. Keep without traversing
unknown Harnery could not establish a safe classification. Keep

Cleanup previews by default. Even with --yes, Harnery rechecks every deletion candidate and refuses to delete entries that changed classification.

A hold protects files whose next step must finish before cleanup can remove them, such as an interrupted transfer or an unresolved handoff. Schema v2 requires a holds array. Each hold records id, reason, set_by (an ArtifactActor), set_at, and expires_at.

Use create --hold --hold-reason to persist protection in the first manifest. Adding a hold after writing files leaves a cleanup window before protection exists. Manual cleanup, session-start cleanup, and review-pack expiry retain held workspaces and their nested evidence. Holds also override byte budgets. release and renew preserve every hold.

hold and unhold use the current agent identity by default. A workflow that outlives an agent session can pass its stable binding identity with --actor. Keep that identity and the exact hold ID in the binding. A later agent acting for that binding can remove its hold; another binding’s holds stay intact. These identities prevent accidental cross-workflow removal. They are local coordination records, not an authentication boundary against someone who can edit the files.

A hold lapses artifacts.hold_days days after it is set (default 14, range 1 to 365; HARNERY_ARTIFACT_HOLD_DAYS overrides it for one process). Choose another window with hold --days <n> (1 to 365) or hold --minutes <n> (1 to 525,600). create --hold uses the default.

Repeating a hold with the same ID, owner, and reason renews it: expires_at restarts from now, and set_at and the owner stay. Reusing its ID with another owner or reason fails. Within 48 hours of a lapse, list and show carry a warning with the exact renewal command:

Terminal window
harn artifacts hold <name> --id <id> --reason "<same reason>" --days 14

When every hold on a workspace has lapsed, it returns to ordinary retention. Its effective expiry is no earlier than the latest lapse, and the size-rule idle grace also counts from that lapse. If retention had already run out, the workspace becomes managed-expired at the lapse, and its reason names the lapsed hold. Lapsed holds stay in the manifest; their owner can unhold them, or renew one to protect the files again.

Holds recorded before hold expiry existed have no expires_at and stay in force. Their rows carry a warning that re-holding starts an expiry. Embedding hosts whose hold mirrors an external lease, such as an open checkout with unsynchronized work, can pass persistent: true to holdArtifact(), or use create --hold <id> --hold-reason <text> --hold-persistent and hold <ref> --id <id> --reason <text> --persistent from the CLI. A persistent hold records persistent: true instead of expires_at, never lapses, stays persistent when repeated, and still counts toward the held budget. Use it only when something outside the artifact store tracks the lease and will remove the hold; an ordinary review hold should lapse and be renewed. unhold requires the recorded owner’s instance_id and removes only the named hold. Repeating that removal after success is harmless. Remove a hold only after the workflow has verified its completion; artifact release alone does not establish that condition.

Embedding clients can call artifactCapabilities() from harnery/core/artifacts or run artifacts capabilities --json. Require schema_version: 2, holds: true, atomic_create_holds: true, and owner_scoped_unhold: true before starting work that depends on holds. hold_expiry: true means holds lapse and must be renewed or made persistent; persistent_holds: true means the CLI accepts --hold-persistent and --persistent; held_budget and disk_free_report mean list and clean report the held budget and free disk. An unknown command or missing capability is a refusal, not permission to continue without protection.

The runtime reads schema v2 only. Schema v1 and unknown versions stay on disk as invalid-manifest; normal operations do not convert them implicitly. artifacts migrate previews valid v1 manifests. artifacts migrate --yes converts each eligible unit to v2 with empty holds and saves its exact old manifest under .harnery/artifact-migrations/<sha256>.v1.json before replacement. IDs, paths, owner records, release state, and retention fields are preserved. The migration records activity before replacing the manifest, preserving the effective deadline as well as the stored retention fields. Tracked files, symlinks, malformed manifests, unexpected v1 hold fields, and unsupported schemas remain untouched. Check each returned row: failed units have action: keep and a reason. A repeated successful migration does nothing.

Version 0.37.0 cleanup rejects schema v2 and therefore retains these workspaces, but it cannot manage their holds. Finish old runtime processes before migration; the new lock cannot coordinate with a cleanup process that predates it.

Earlier v1-to-v2 migrations treated the manifest replacement as fresh activity, extending the effective deadline by another full retention window. artifacts repair-activity previews corrections. Pass --yes to apply them; the command changes metadata only and never deletes payload files.

A correction requires a hash-verified v1 preimage, an exact match to its v2 conversion, and unchanged root and manifest timestamps matching the migration. The command skips artifacts with later metadata changes, holds, tracked files, unverifiable preimages, or later root-directory changes. It derives activity from surviving payload timestamps and the original creation or renewal anchor. Old root-only activity preceding the migration cannot be reconstructed because the old migration did not save that timestamp. This is an explicit recovery operation, not a silent change during inventory.

Every correction saves the original v2 manifest and the before/after deadlines in .harnery/artifact-migrations/*.activity-repair.json. Repeated repairs leave checkpointed artifacts unchanged. Review artifacts clean after repair; applying cleanup remains a separate artifacts clean --yes operation with the existing hold, owner, tracked-file, and filesystem guards.

Manifest mutation and deletion share .harnery/artifacts-mutation.lock. Each lock records its hostname, process ID, and process-start token in a unique owner file. The next writer can recover a dead local owner’s lock. Live owners, foreign hosts, unreadable owners, symlinks, and empty locks remain protected; age alone never permits removal. Competing recovery attempts cannot remove a replacement owner’s lock.

A live PID counts as recycled only when boot-scoped Linux process ticks prove it belongs to a different process. Wall-clock ps tokens are not enough; platforms without boot-scoped tokens recover only after the PID is gone.

An empty lock can come from an older runtime or an interruption while publishing or removing its owner file. There is no safe automatic way to distinguish that from a writer initializing the lock. Establish that artifact operations have stopped before explicitly removing that exact empty directory. Do not recursively delete lock contents. Normal exceptions release the lock; interrupted work with a valid owner record recovers automatically.

Use harn journal for short text breadcrumbs that help with context recovery or peer handoff. Use harn artifacts for file trees that can expire. If a report, input, or piece of evidence becomes part of the project’s record, copy it to the appropriate tracked path and document what it contains. Local artifact retention is not durable storage.