@hasna/snapshots
v0.2.1
Published
Runtime snapshot and restore layer for Hasna local open-source developer environments.
Maintainers
Readme
@hasna/snapshots
Runtime snapshot and restore layer for the local Hasna open-source developer environment.
@hasna/snapshots captures a point-in-time view of local runtime resources, stores it in SQLite, and can build guarded restore plans for the parts that are safe to recreate automatically.
What It Captures
- machine identity and runtime context
- tmux sessions, windows, panes, and pane working directories
- per-pane resume identity: the newest opencode2 session (
session_v2in~/.local/share/opencode/opencode.db) and the newest Claude Code JSONL (~/.claude/projects/<slug>/) whose recorded working directory matches the pane — so a tmux layout destroyed by an outage can be rebuilt by resuming the agent sessions that were live in each pane - Hasna project registry output when
projects list --jsonis available - local process summaries with secret-like arguments redacted
- session, browser, and desktop state when companion Hasna CLIs or state folders exist
- diagnostics for optional integrations that are absent or unavailable
Missing optional tools do not fail a snapshot. They become diagnostic resources so the snapshot still explains what was visible at capture time.
Safety Model
Restore is dry-run by default.
The built-in restore allowlist is intentionally narrow:
projectresources can plan creation of missing project directories.tmux-session,tmux-window, andtmux-paneresources can plan detached tmux layout recreation.- other resource kinds are observed and skipped unless future adapters explicitly support them.
Execution requires both --apply and --yes. Restore never kills existing sessions or overwrites existing service files. Existing project directories and tmux sessions become noop operations, and child tmux resources are blocked by default when the target session already exists. Use --merge-existing only when you explicitly want to mutate a live tmux session.
tmux restore is best effort. The default mode is layout-only: it recreates sessions, windows, panes, working directories, layouts, and active selection where safe, but it does not replay captured commands. Use --tmux-mode resume-marked to replay only commands that were explicitly marked restartable.
Install
bun install
bun run buildThe package stores data in snapshots.sqlite by default. The data home is
resolved through the @hasna/paths resolver — the XDG data home
~/.local/share/hasna/snapshots once adopted, otherwise the legacy
~/.hasna/snapshots (an existing local store never becomes invisible on
upgrade). The exact-app HASNA_SNAPSHOTS_DIR override and the data-kind
HASNA_DATA_HOME override are honored by the resolver. Override the store
path with:
HASNA_SNAPSHOTS_DIR=/path/to/data
HASNA_SNAPSHOTS_DB_PATH=/path/to/snapshots.sqliteResume-identity sources and the restore freshness gate can also be configured:
HASNA_SNAPSHOTS_OPENCODE_DB=/path/to/opencode.db # default ~/.local/share/opencode/opencode.db
HASNA_SNAPSHOTS_CLAUDE_PROJECTS_DIR=/path/to/projects # default ~/.claude/projects
HASNA_SNAPSHOTS_MAX_AGE=72h # default restore max-age (duration string)
HASNA_SNAPSHOTS_CAPTURE_LEASE_TTL_MS=240000 # capture lease TTL (default 240s)
HASNA_SNAPSHOTS_CAPTURE_LEASE_WAIT_MS=30000 # max wait for a live capture lease (default 30s)Captures against one store are serialized by a short-lived SQLite lease
(capture_leases table, same watchdog pattern as the fleet's cron guards):
a concurrent capture (e.g. the */5 cron firing while a manual capture is in
flight) waits for the holder, and a crashed holder's lease self-expires
after the TTL. If the lease cannot be acquired within the wait window the
capture still proceeds — saveSnapshot is idempotent, so an overlapping
duplicate snapshot becomes a no-op (recorded as a capture.lease-unavailable
audit event) instead of a failed transaction.
SDK
The package exposes typed SDK exports from the root package and focused subpaths:
import { SnapshotStore, captureAll, createRestorePlan } from "@hasna/snapshots";
import { planService } from "@hasna/snapshots/service";Capture and store a snapshot:
import { SnapshotStore, captureAll } from "@hasna/snapshots";
const store = new SnapshotStore({ path: "/tmp/snapshots.sqlite" });
try {
const capture = await captureAll({
include: ["machine", "projects", "tmux"],
tmuxPaneTailLines: 0,
});
const snapshot = store.saveSnapshot(capture.resources, {
name: "before-upgrade",
diagnostics: capture.diagnostics,
sourceStatuses: capture.sourceStatuses,
});
console.log(snapshot.id, snapshot.resourceCount);
} finally {
store.close();
}Build a guarded restore plan without applying it:
import { SnapshotStore, createRestorePlan } from "@hasna/snapshots";
const store = new SnapshotStore();
try {
const [snapshot] = store.listSnapshots(1);
if (!snapshot) throw new Error("No snapshots found");
const resources = store.getSnapshotResources(snapshot.id);
const policies = store.listPolicies();
const plan = createRestorePlan(snapshot, resources, policies, {
include: ["tmux-session:work"],
dependencyMode: "parents",
targetMode: "strict",
});
store.saveRestorePlan(plan);
console.log(plan.id, plan.summary, plan.autopilot);
} finally {
store.close();
}Service planning is SDK-only until callers explicitly write/apply the generated plan:
import { planService } from "@hasna/snapshots/service";
const plan = planService({ intervalSeconds: 300 });
console.log(plan.kind, plan.path, plan.note);Interface Parity
| Workflow | CLI | SDK | MCP | HTTP server |
| --- | --- | --- | --- | --- |
| Capture snapshot | snapshots capture, snapshots daemon once | captureAll, SnapshotStore.saveSnapshot | capture_snapshot | POST /snapshots |
| List snapshots | snapshots list | SnapshotStore.listSnapshots | list_snapshots | GET /snapshots |
| List capture runs | snapshots runs | SnapshotStore.listCaptureRuns | not exposed | not exposed |
| Capture freshness | snapshots freshness | freshness | not exposed | not exposed |
| Read snapshot/resources | snapshots show, snapshots resources | SnapshotStore.getSnapshot, SnapshotStore.getSnapshotResources, SnapshotStore.listResources | get_snapshot | GET /snapshots/:id |
| Plan restore | snapshots plan, snapshots restore without --apply | createRestorePlan, SnapshotStore.saveRestorePlan | plan_restore | not exposed |
| Apply restore | snapshots restore --apply --yes | createRestorePlan(..., { apply: true, yes: true }) | intentionally not exposed | not exposed |
| Policy management | snapshots policy list/set | SnapshotStore.listPolicies, SnapshotStore.upsertPolicy | intentionally not exposed | not exposed |
| Service planning | snapshots service plan/status/install | planService, serviceStatus, applyServicePlan | intentionally not exposed | not exposed |
MCP intentionally exposes only capture, list, read, and plan tools. Restore execution and service installation mutate local machine state, so they stay in the CLI/SDK surfaces where callers must opt in explicitly.
CLI
snapshots capture --name before-upgrade
snapshots list
snapshots runs --limit 5
snapshots freshness --threshold 900
snapshots show <snapshot-id>
snapshots resources --limit 50
snapshots resources <snapshot-id> --tree
snapshots plan <snapshot-id> --resource kind:tmux-session --with-dependencies
snapshots restore <snapshot-id> --resource tmux-session:work --with-dependencies --apply --yes
snapshots restore --plan <plan-id> --plan-hash <hash> --apply --yes
snapshots restore <snapshot-id> --max-age 72h
snapshots policy list
snapshots policy set kind:process ignore --reason "processes are observe-only"
snapshots daemon once --tmux-tail-lines 0
snapshots daemon run --interval 300 --tmux-tail-lines 20
snapshots service plan
snapshots service install --apply --yes
snapshots service statusAll commands emit JSON so agents can consume stable contracts.
Snapshot summaries include per-source status, duration, resource count, diagnostic count, and a degraded flag when a source returns warnings or errors. Daemon captures can use --tmux-tail-lines 0 to skip pane scrollback tails for faster topology snapshots.
Restore plans include an autopilot assessment. By default, only low-risk project directory creation can be marked safe for autopilot. tmux mutations require approval, and shell command replay is forbidden for autopilot.
Restore carries a freshness gate: --max-age <duration> (or
HASNA_SNAPSHOTS_MAX_AGE) refuses snapshots older than the configured limit
with a logged, audit-trailed error instead of planning or applying a stale
restore. Durations accept ms, s, m, h, d, and compounds like
1h30m; a bare number means seconds. The gate is recorded on the plan and
re-checked when a saved plan is applied.
MCP
snapshots-mcp provides a minimal stdio JSON-RPC bridge with these tools:
capture_snapshotlist_snapshotsget_snapshotplan_restore
Local HTTP Server
snapshots-serve
curl http://localhost:7337/health
curl http://localhost:7337/snapshots
curl -X POST http://localhost:7337/snapshots -H 'content-type: application/json' -d '{"name":"manual"}'Release Verification
See the hardening roadmap ledger guide for the 17-item coordination, evidence, blocker, and closure requirements.
bun run typecheck
bun test
bun run build
bun run verify:releasePublishing still depends on normal external credentials for npm and GitHub.
