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.
Subcommands
Section titled “Subcommands”harn artifacts create <slug> --purpose <text> [--days <n> | --minutes <n>] [--big [--allow-low-disk]]harn artifacts adopt-unmanaged [--days <n>] [--big] [--yes]harn artifacts listharn 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 --jsonharn 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/.
Typical workflow
Section titled “Typical workflow”# 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 --yesRetire disposable evidence after review
Section titled “Retire disposable evidence after review”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:
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:
harn artifacts create layout-probe --purpose "Temporary layout check" --minutes 90harn 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.
Remove one workspace immediately
Section titled “Remove one workspace immediately”When reviewed files are no longer needed, the creating agent can preview removal of one workspace, then delete it immediately:
harn artifacts remove <artifact-id> --reason "Reviewed; no remaining use"harn artifacts remove <artifact-id> --reason "Reviewed; no remaining use" --yesPreview 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.
Deliver review files consistently
Section titled “Deliver review files consistently”Create a compact Markdown card with no setup:
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:
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:
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.
Size budgets
Section titled “Size budgets”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 --bigwhen a job is expected to exceed it, or runartifacts 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 bytes and free disk
Section titled “Held bytes and free disk”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, andheld_over_budget.largest_holds: the five largest held workspaces, each with itsname,bytes, andholds(id,set_by,expires_at,persistent).free_byteson the artifact store’s filesystem (null when it cannot be read),min_free_bytes, andlow_disk.warnings: plain sentences naming what to release or prune. The count of rows that carry their ownwarningisrow_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:
harn artifacts create renders --purpose "Full-resolution renders" --big# refused: names free space, the floor, and the commands that free spaceharn 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.
Adopt loose files
Section titled “Adopt loose files”Older integrations sometimes wrote regular files directly under
.harnery/artifacts/. Preview their adoption with:
harn artifacts adopt-unmanagedharn artifacts adopt-unmanaged --yesThe 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.
Automatic cleanup
Section titled “Automatic cleanup”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.
Cleanup classes
Section titled “Cleanup classes”| 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.
Hold work that is not ready for cleanup
Section titled “Hold work that is not ready for cleanup”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.
Hold expiry and renewal
Section titled “Hold expiry and renewal”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:
harn artifacts hold <name> --id <id> --reason "<same reason>" --days 14When 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.
Migrate existing manifests
Section titled “Migrate existing manifests”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.
Repair activity after an older migration
Section titled “Repair activity after an older migration”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.
Journal, artifacts, and durable files
Section titled “Journal, artifacts, and durable files”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.