Release Notes
Halfhand 1.0.0. The first stable release. From here on the contracts in
STABILITY.md hold: CLI flags/exit codes, --json output, the on-disk
database, MSRV, and the hh-core Rust API change only additively (or via a
documented deprecation). A database written by v0.1.0-beta.1 opens and
reads correctly under 1.0 — proven by the permanent
hh/tests/migration_from_beta.rs against a committed beta-created fixture.
Highlights
- Secret redaction (
hh scan,hh redact): detect secrets in recorded
sessions, report them without ever printing the value, and irreversibly
redact in place — events and blobs rewritten, originals securely deleted,
DB compacted. Exits4on findings so CI can gate on a clean scan. - Export / HTML replay (
hh export --bundle|--html,hh import):
portable.hharchives (manifest + events + referenced blobs, zstd), a
self-contained single-file HTML replay page (hh replay --web), andhh importto bring a bundle back under a new id. Exports are always
redacted by default; opting out needs an interactive TTY confirmation. - Codex CLI, Gemini CLI, and Claude Desktop adapters in addition to
Claude Code, each with a fuzz target that never panics on malformed input. - Cross-session FTS5 search (
hh search): words, phrases, prefix*,
AND/OR/NOT, with--agent/--kind/--since/--pathfilters and--json. - Windows support (build-only per SRS §2.2): CI builds on
windows-latestevery push; PTY recording on Windows remains best-effort. - Stability contract (
STABILITY.md): the 1.0 promise for CLI,--json
(schema frozen at2), the on-disk DB (forward-only migrations),
MSRV (1.75, minor-release bumps only), andhh-coresemver (enforced by
thecargo-semver-checksCI job). - Release pipeline via cargo-dist: per-target binaries (mac/linux
gnu+musl, windows msvc), SHA-256 checksums, shell + powershell installers,
a Homebrew formula, and keyless GitHub artifact attestations
(gh attestation verify).hh completions <shell>and a generatedhh.1
man page ship in every archive. - Docs site (mdbook) on GitHub Pages, with a printable hand-run
docs/manual-qa.mdchecklist anddocs/1.0-definition-of-done.mdmapping
every release bar to its proof.
Added — 1.0 release-readiness
hh completions <shell>: prints a shell completion script (bash, zsh,
fish, powershell, elvish) viaclap_complete(FR-6.2 ergonomics). New
docs/completions.mdpage;--helpexample per shell. The scripts and the
hh.1man page are also generated at build time by the newhh-dist
workspace crate (just dist-assets) and shipped in release archives.- cargo-dist release pipeline: tag-triggered (
v*) releases now produce
per-target binaries (mac/linux gnu+musl, windows msvc) with the git sha
embedded (HH_BUILD_SHA, NFR-8), SHA-256 checksums, a shell + powershell
installer, keyless GitHub artifact attestations (verify withgh attestation verify), and an auto-pushed Homebrew formula to
halfhand-org/homebrew-tap. Config lives indist-workspace.toml; the
workflow is generated bycargo dist generate. See CONTRIBUTING.md
"Cutting a release". - mdbook docs site under
docs/, published to GitHub Pages on release
(and on push to main as a preview) via.github/workflows/docs.yml. The
existingdocs/*.mdanddocs/adr/*.mdare rendered in place (no path
moves, preserving the 93 in-repo references); new pages: Quickstart,
Recording, Replay & Inspect, Adapters, FAQ, Stability, Security. - Repo hygiene: bug-report and feature-request issue templates (the bug
form requireshh --versionandhh statsoutput), a PR template encoding
the CLAUDE.md done-definition,CODE_OF_CONDUCT.md(Contributor Covenant
2.1), andCONTRIBUTING.md(dev setup, adapter fixture-capture guide, fuzz/
bench instructions, release runbook). hh-distcrate: apublish = falseworkspace crate that generates
completion scripts and the man page from the singleClidefinition, so
cargo install halfhandstill installs onlyhh.
Changed — 1.0 release-readiness
- README rewritten for 1.0 (245 → 116 lines): hero, install matrix
(cargo/brew/script), 30-second demo, the honest "faithful playback" framing,
badges, and a short "why local-first" section. Depth moved to the docs site. - Release workflow replaced: the bespoke
.github/workflows/release.yml
is superseded by cargo-dist's generated workflow; re-runcargo dist generateto emit it fromdist-workspace.toml. just fuzz-allnow runs all eight fuzz targets (was missing
codex_jsonl,gemini_jsonl,import_bundle).
Added — 1.0 stability groundwork
STABILITY.md: the 1.0 promise for CLI flags/exit codes (additive-only,
deprecate-before-remove),--json(schema frozen, additive-only, see
below), the on-disk DB (forward-only automatic migrations;--jsonand
hh exportbundles — not raw SQL — are the supported programmatic
interface), MSRV (bumps are minor releases only), and hh-core's Rust API
(enforced today by thecargo-semver-checksCI job).- JSON schema bumped to
schema:2, frozen for the 1.0 series: additive
changes (new fields, new enum values, newbodyshapes) no longer bump the
integer, reversing the beta-era policy.schema:2folds in drift that
shipped without a version bump in the previous entry below — three new
agent_kindvalues and theredaction_auditlifecycle body — documented
field-by-field with a diff fromschema:1indocs/json.md. A new
hh-core::event::JSON_SCHEMA_VERSIONconstant is now the single source of
truth every JSON-emitting call site (includinghh-core::bundle, forhh export --bundle) reads from, replacing several independently hardcoded
1s. - Docs/code reconciliation audit:
README.md's command table was missing
hh gc/hh stats/hh import/hh searchand still called the Codex/
Gemini/Claude Desktop adapters "planned for v0.2" after they'd shipped
(see the entry below);docs/json.md'sagent_kindtable and
docs/search.md's--agentfilter list were missingclaude-desktop(and
docs/search.mdalso missingmcp-only);hh mcp-proxy --session-hint's
--helptext implied it was stored when it is in fact accepted but not
yet persisted anywhere (now documented honestly indocs/mcp-proxy.md). hh-core::deprecation::warn_deprecated: the standard stderr format for
a deprecation warning (hh: warning: <what> is deprecated; <guidance>),
deduplicated per unique warning id so a call site can never print more than
once per invocation.Config::load's legacyhalfhand.toml/hh.toml
fallback notice now goes through it.- CLI conformance test (
hh/tests/cli_conformance.rs): snapshot-tests
hh --helpand everyhh <sub> --help, and asserts the documented exit
codes —2usage error,3session not found,4hh scanwith
findings.
Added — Codex CLI, Gemini CLI, and Claude Desktop adapters; cross-session search (docs/search.md, docs/adr/0003-fts-index-strategy.md)
- Codex CLI adapter: tails
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl,
convertingresponse_item(message, reasoning, function_call,
custom_tool_call) andevent_msg(user_message, agent_message,
token_count, exec_command_end) records to the same structured event model
as the Claude Code adapter (agent_kind = codex-cli). - Gemini CLI adapter: tails
~/.gemini/tmp/<hash>/chats/session-*.jsonl,
convertinguser/geminirecords (tool calls, thoughts, tokens)
(agent_kind = gemini-cli). - Claude Desktop adapter: reuses the Claude Code JSONL tailer/parser
(identical transcript format) under its ownagent_kind = claude-desktop
sohh list/hh inspectdistinguish the two. - Force any of the four with
hh run --adapter <claude-code|claude-desktop| codex-cli|gemini-cli> -- <command>; auto-detection matches on the wrapped
command's basename same as before. hh search <query> [--agent] [--kind] [--since] [--path] [--limit] [--json]: FTS5 full-text search across every recorded session's event
summaries and message bodies, with highlighted snippets in the table view.
Backed by a newevents_ftsvirtual table (migration 0004, additive) kept
incrementally in sync by the writer thread on every append, and maintained
(rows removed) onhh deleteandhh redact— a redacted secret is
provably not findable via search (property-tested).--limitdefaults to
50, capped at 500.- Fuzz targets for the new Codex/Gemini JSONL parsers (
fuzz/fuzz_targets/ codex_jsonl.rs,gemini_jsonl.rs) and a search benchmark (target <100ms
across 100 sessions × 1000 events). AgentKindmarked#[non_exhaustive](new variants append at the end, so
existing discriminants don't shift);cargo-semver-checksCI excludes the
one-timeenum_marked_non_exhaustivelint for it.
Added — redaction pipeline (SRS §8, docs/redaction-design.md)
- Secret detection engine (
hh-core::redact): compiled detectors for
AWS access key ids, GitHub/GitLab/Slack tokens, PEM private-key blocks
(including truncated fragments), JWTs, a conservative high-entropy
scanner, and user-defined regexes via the new config[redaction] rules.
Matches are replaced with{{REDACTED:<type>:<hash8>}}wherehash8is
a BLAKE3 prefix of the secret — one-way, but stable, so the same secret
correlates across events without being stored. Contract (property-tested
and fuzzed via the newredact_detecttarget): false positives are
acceptable, false negatives on the named token types are bugs. hh scan <session|last|--all> [--json]: reports detected secrets
(type, step, location, hash8) without ever printing them. Exit code 4
when findings exist — the documented redaction/policy exit code — so CI
can gate on a clean store.hh redact <session> [--yes]: irreversibly redacts a recorded
session in place — rewrites events and affected blobs (refcounts move
with the references), securely deletes originals (best-effort zero
overwrite + unlink), appends aredaction_auditlifecycle event, and
checkpoints +VACUUMs so no plaintext survives in the WAL or freelist.
Property-tested: after redact, seeded secrets are undetectable anywhere
inhh.dbor the blob store.hh export [session] [--out FILE] [--html] [--no-redact]: exports a
session as aschema:1JSON bundle or a self-contained HTML page.
Redacted by default, always — the whole bundle passes through one
redaction chokepoint before a byte is written.--no-redactrequires an
interactive typed confirmation and is refused on a non-TTY stdin (no
bypass flag, by design).- Record-time redaction (opt-in, config
[redaction] at_record = true):
matches are scrubbed before hitting disk — event summaries/bodies on
the writer task, blob content before hashing/compression. - New docs:
docs/redaction.md(threat model, what is/isn't caught,
config),docs/redaction-design.md(invariants), and a repo-root
SECURITY.mdwith the disclosure policy. - New dependency:
regex(linear-time matching — no catastrophic
backtracking on hostile recorded input).
Added — portable session bundles, hh import, interactive HTML replay (docs/export-import.md)
hh export --bundle [-o FILE]: a portable, deterministic session
archive — a zstd-compressed tar ofmanifest.json(format version,hh
version, session metadata, integrity hashes) +events.ndjson(one
compact JSON object per event, position-indexed since DB row ids don't
survive import) + every referenced blob, content-addressed. Byte-identical
on repeat export of an unchanged session (fixed entry order, fixed
tar-header metadata, no wall-clock fields). Redacted by default through
the same chokepoint as plainhh export/--html; if redaction changes a
blob's hash, every reference to it in the bundle is rewritten to match.
hh export <session>with no flags is unchanged (still plain JSON to
stdout) —--bundleis additive, opt-in, and refuses to write its binary
output to an interactive terminal without-o.hh import FILE: validates a bundle end to end (bounded zstd
decompression, a tar-entry allow-list, per-blob BLAKE3 verification, an
events.ndjsonintegrity digest, and full referenced-blob-exists checks)
before importing it under a brand-new local session id — the original id
is preserved in the newsessions.imported_fromcolumn/JSON field
(additive migration 0003). Property-tested: export → import → export
round-trips every event's content and blob bytes exactly. Fuzzed (new
import_bundletarget): never panics on malformed/corrupt/hostile input.hh export --htmlredesign: from a static "step report" into a real
self-contained interactive replay page — a clickable step timeline, a
detail pane (pretty JSON / a real diff viewer with precomputed hunks /
plain text depending on event kind),j/kkeyboard navigation mirroring
the replay TUI, and a dark-by-default theme with a light toggle. Zero
network requests, no CDN, no build step (hand-written vanilla JS). Session
data — untrusted input, since it's whatever the recorded agent/tool output
contained — is embedded once as escaped JSON inside a
<script type="application/json">tag and only ever reaches the DOM via
createElement/textContent, neverinnerHTML, so a<script>tag or
event-handler attribute anywhere in recorded content renders inert.hh replay --web: sugar forhh export --html, writes the page to a
temp file and prints the path. Needs no terminal (unlike the TUI); does
not auto-open a browser or start a server.- New dependency:
tar(the same crate Cargo itself uses for package
tarballs — pure Rust, no network, no unsafe in the paths used here).
Diagnostics
- A degraded Claude Code adapter session now self-documents in the database,
not just on stderr: the specific degrade reason is persisted as anerror
event (body_json = {"adapter":"claude-code","reason":"…"}), so
SELECT … WHERE kind='error'explains why a session recorded no structured
steps — previously such a query returned empty and the failure was a silent
mystery. Reasons are now specific:no jsonl matched cwd slug <slug>(with
the slug dir it looked in + candidate counts),jsonl found (<file>) but 0 records parsed (read N line(s); first parse error at line K: <msg>),
found a transcript at <file> but could not read it, ordiscovery selected file <file> but it's a directory. A found-but-empty transcript now degrades
with0 records parsedinstead of finalizingactivewith 0 steps. Set
HH_DEBUG=1duringhh runto capture a discovery+parse trace (computed
slug, projects dir, candidate files, selected transcript, records read,
events produced, first conversion failure) to stderr. Note: a degraded
session now reports 1 step (theerrorevent itself) rather than 0.
Fixed
- Silent recording breakage (the reported "0 steps / 0 files but
status=ok" symptom): the Claude Code adapter's transcript locator used a
fixed 3 s deadline and matched the firstcwd-bearing record, so on
recent Claude versions — which write~/.claude/projects/<slug>/*.jsonl
lazily (only on first output) and omitcwdfrom the earlysystem/meta
records — the locator either timed out before the file appeared or rejected
it on the cwd-less head, finalizedokwith 0 structured steps, and printed
no warning. The locator now polls until the session stops, snapshots
pre-existing transcripts so it only matches one created during this
session, and scans past cwd-less records to the first cwd-bearing one (and
verifies it belongs to this session's cwd). Every degrade path now carries
an actionabledegrade_reasonprinted after the child exits (FR-1.5), and
thehh runepilogue warns when an adapter-active claude-code session
records 0 steps over >60 s. - A
halfhand.toml(orhh.toml) config file is no longer silently ignored.
Whenconfig.tomlis absent but a legacy file exists in the same directory,
hhnow loads it as a fallback (so ignore globs / a customdata_dirtake
effect) and emits a single one-line deprecation hint on stderr suggesting the
rename toconfig.toml. Whenconfig.tomlis present, a sibling legacy
file is still genuinely ignored (the canonical path wins) —hhwarns on
stderr at startup andhh doctorreports it as a failing check, naming the
ignored file and where to move its contents. - The FS watcher no longer aborts
hh runwith "recording failed" when its
cwd is unwatchable (e.g.notifyrejects a recursive watch withEACCES).
It degrades instead: a single stderr warning points athh doctor, file
recording is skipped, and the PTY + adapter session still records
(status=ok). A per-directory fallback also keeps file recording working
when one unreadable subdir would have blacked out the whole tree. - The FS watcher now drains in-flight events for a bounded window (150 ms of
silence, capped at 1 s) after the child exits, before finalizing, and then
runs a deterministic re-scan ofcwdas a backstop. On macOS, FSEvents can
deliver a write event after the process that performed it has already
exited — or never deliver it at all on GitHub's macOS runners. The old
behavior stopped the watcher the instant the child was gone and dropped that
event, so a quick-exiting agent could finalize with "0 file changes" and
hh inspect --diffreported "no file changes in this session" despite files
being written. The grace drain catches late events; the re-scan backstop
catches the absent-event case deterministically (it does not depend on
notifydelivering anything). At startup the watcher now hashes every
capturable baseline file (storing only the hash, not a blob), so the backstop
can also detect a missed modify of a pre-existing file (current hash ≠
baseline hash) and record it asModified— a backstopped modify renders
with a missing before-side ("all added"), since the original content was
overwritten before it was observed. Files already captured via the event
path and unchanged baseline files are skipped, so neither mechanism
duplicates a normal-path capture. The startup baseline is captured
synchronously inspawn_watcherbefore it returns (on the caller's thread,
not the worker), so any file written after the watch starts — by the recorded
child or otherwise — is guaranteed post-baseline and is never misclassified
as a pre-existing file the backstop would skip as "unchanged"; this also
closes the scheduling-dependent window that made the macOS-CI capture test
flaky. A prompt backend (Linux inotify) pays only the short quiet wait; the
startup + shutdown baseline hashing is the trade-off for missed-modify
coverage on large trees. hh listcolumn headers no longer drift out of alignment with the data
when color is enabled: padding now measures visible width (ANSI escapes
stripped) instead of byte length, so colored status cells no longer push
every column to their right out of line.hh listalso shows a⚠marker on
rows whose adapter endeddegraded, so a silently-broken session is no
longer indistinguishable from a clean one.BlobStore::get/remove_if_unreferencednow reject a malformed hash
(wrong length, non-hex, or non-ASCII bytes) instead of risking a byte-slice
panic on a multi-byte UTF-8 character at offset 2, or a path-traversal write
via a crafted hash string.- The FS watcher, adapter tailer, and MCP proxy threads share one writer
Mutex; a panic in any one of them no longer poisons it for the rest of the
session (every lock site now recovers from poisoning) or abortshh run's
finalize — a panicked adapter tailer now degrades the session
(adapter_status=degraded, warning) exactly like the existing
"no transcript found" path, instead of leaving it stuck at
status=recordingforever. writer_run'sPRAGMA foreign_keys = ONfailure is now logged instead of
silently swallowed (a silent failure here would disable FK enforcement for
the rest of the session with no visible cause).- Bumped
crossbeam-epoch0.9.19 → 0.9.20 (RUSTSEC-2026-0204), pulled in
transitively viaignore.
Performance
- Ingest throughput:
PRAGMA synchronous = NORMAL(NFR-1 / NFR-3). The store
never setsynchronous, so SQLite defaulted toFULL, fsyncing the WAL on
every event commit. That met NFR-3's durability ("fsync on session finalize")
and then some, but capped sustained ingest near ~300 events/s — far below
NFR-1's ≥5,000/s.Store::openand the writer thread now set
synchronous = NORMAL: in WAL mode this keeps ACID (no corruption on crash)
but skips the per-commit fsync, fsyncing only at checkpoint. That matches the
durability design NFR-3 actually names —finalize_sessionruns
wal_checkpoint(TRUNCATE), which fsyncs, so a session is durable at finalize;
SQLite's defaultwal_autocheckpoint(≈4 MiB) bounds the mid-session
power-loss window to the last autocheckpoint. Sustained ingest is now ~1.5k
events/s (≈5× over the per-event-fsync baseline); reaching NFR-1's 5k/s still
needs statement caching + a batched writer (tracked as follow-up; the
regression gate below protects the gain). No CLI/JSON/schema/env change. hh inspect --jsonnow streams a session as NDJSON through
Store::for_each_event_detail(oneEventDetailat a time) instead of
collecting the whole session into RAM — constant memory for any session size
(Area 2). A 100k-event session streams without materializing 100k rows.- The replay body cache (
ReplayData) is verified bounded at its 50-entry
capacity across unbounded scrolling (Area 2): a regression to an unbounded
cache would failget_evicts_lru_to_stay_bounded_at_capacity.
Added
hh doctor(read-only diagnostic): runs five health checks — data dir
writability,PRAGMA integrity_check, config resolution + non-canonical
config detection, Claude Code transcript discoverability for the cwd (with a
parse-test of the newest transcript), and anotifywatcher smoke test —
printing one✓/✗line per check and exiting nonzero if any fail.--json
emits a stableschema:1object with a per-check array. Docs: see
docs/doctor.md;--helpcarries an example.- Regression tests for the silent-breakage fix: an end-to-end file-change
recording test, an adapter test against both Claude JSONL fixture
generations (including the new-format transcript withmode/
permissionMode/fileHistorySnapshotand nocwdon the early records),
an integration test asserting the watcher-init-failure warning reaches
stderr and the session still recordsok(not "recording failed"), and an
adapter degraded-warning-at-finalize test.hh list/hh doctorrendering
is locked with insta snapshots. cargo fuzztargets (fuzz/, nightly toolchain) for the four
untrusted/external-input parsers: Claude JSONL transcript lines, MCP
JSON-RPC framing,config.toml, and blob decompression. Seeded from
tests/fixtures/; runs nightly in CI (60s/target,
.github/workflows/fuzz-nightly.yml) and viajust fuzz-all <seconds>
locally.proptestproperty tests: blob refcount/GC invariants (refcount tracks live
references exactly; GC removes a blob iff unreferenced) across arbitrary
create/append/delete sequences, step-assignment invariants (dense ordinals,
exact call/result pairing, idempotent) across arbitrary event interleavings,
and migration-reopen idempotency (byte-identical schema across N reopens).- Panic-injection tests proving a real panic on the FS watcher thread or the
Claude adapter tailer thread degrades the session instead of aborting the
recording. - CI:
cargo-llvm-covgate (hh-core ≥ 80% lines, fail-under; workspace-wide
report-only) andcargo-semver-checksagainstmainfor hh-core's public
API (adapters implement itsAdaptertrait). - Criterion benches (
hh-core/benches/) + a nightly regression gate (Area 1 /
NFR-1): four groups —ingest(append_eventthroughput),replay_index
(10k/100k-eventlist_event_index),blob_write/blob_read(1 KiB/1 MiB),
andadapter_jsonl(Claude JSONL conversion, gated on thefuzzingfeature
so it uses the dev-only parser entry point, off the semver-checked surface).
.github/workflows/bench-nightly.ymlrunscargo bench -p hh-core --features fuzzingnightly and fails on >15% regression vs the previous run's baseline
(same runner class via anactions/cacherolling baseline — a committed
baseline would false-positive across hardware).just bench/
just bench-comparerun the same locally. Dev-only: criterion is a
dev-dependency (default-features off) and pulls no HTTP client, so NFR-2's
no-network-crate check stays green. Seedocs/performance.md. hh gc(reclaim space, Area 3): prunes orphaned blob files and stale
blobsrows — the crash-leftover caseshh deletedoes not reach (a blob
written to disk betweenBlobStore::putand the referencing event's commit,
or ablobsrow whose backing file was deleted out of band) — andVACUUMs
hh.dbto shrink it on disk (--no-vacuumskips the rebuild). Safe: it only
removes blobs no live event references, never referenced data.--jsonemits
a stableschema:1object (orphan_files_removed/orphan_bytes_reclaimed
/orphan_rows_removed/vacuumed). Docs: seedocs/gc.md;--help
carries an example.hh stats(read-only store inventory, Area 3): session/event/blob counts,
on-disk footprint (hh.db+ WAL/SHM sidecars + compressed blob directory),
and the largest sessions by event count (--top N, default 5).--json
emits a stableschema:1object withblobs/disk/largest_sessions
sub-objects. Docs: seedocs/stats.md;--helpcarries an example.- Storage schema migration 0002 (additive): a partial index
idx_events_needs_healonevents(session_id) WHERE step IS NULL AND kind != 'terminal_output', makingStore::open's step self-heal probe
O(rows-needing-heal) instead of an O(all-events) scan on every invocation —
including read-onlyhh list, whose cold start used to scale with total
recorded history. Additive (a new index on an existing table); no breaking
schema change, and theschema_versionrow is the only data write.
Install halfhand 1.0.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/halfhandorg/halfhand/releases/download/v1.0.0/halfhand-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/halfhandorg/halfhand/releases/download/v1.0.0/halfhand-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install halfhand-org/tap/halfhandDownload halfhand 1.0.0
| File | Platform | Checksum |
|---|---|---|
| halfhand-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| halfhand-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| halfhand-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| halfhand-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| halfhand-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| halfhand-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
| halfhand-aarch64-unknown-linux-musl.tar.xz | ARM64 MUSL Linux | checksum |
| halfhand-x86_64-unknown-linux-musl.tar.xz | x64 MUSL Linux | checksum |
Verifying GitHub Artifact Attestations
The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:
gh attestation verify <file-path of downloaded artifact> --repo halfhandorg/halfhandYou can also download the attestation from GitHub and verify against that directly:
gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>