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]
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 capabilities --json
harn artifacts create <slug> --purpose <text> --hold <id> --hold-reason <text> --actor <instance-id>
harn artifacts hold <ref> --id <id> --reason <text> [--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.

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 retained managed bytes exceed it, Harnery selects inactive workspaces by earliest effective expiry until the remainder fits.
  • One workspace has a 1 GiB ceiling. Pass create --big when a job is expected to exceed it. An inactive oversized workspace without that acknowledgement becomes a cleanup candidate even before its time window expires.

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

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

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.

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. 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 remains, 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 An inactive unit exceeds the per-bundle ceiling without --big. Delete
managed-over-budget The repository exceeds its managed byte budget and this inactive unit 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), and set_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.

Repeating an identical hold is idempotent and preserves its original timestamp. Reusing its ID with another owner or reason fails. 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. 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.