How It Works¶
This page covers the internals of handoff: where data lives, how the database is structured, how IDs are generated, how expiry works, and the design principles the tool is built around.
Storage¶
All data is stored in a single SQLite file. The location depends on your operating system:
The directory is created automatically the first time any handoff command runs. No setup step is required.
handoff uses modernc.org/sqlite — a pure-Go SQLite driver that bundles the SQLite engine directly into the binary. There is no CGO dependency and no requirement to have SQLite installed on the host system.
Database schema¶
CREATE TABLE IF NOT EXISTS packages (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
summary TEXT DEFAULT '',
content TEXT NOT NULL,
tags TEXT DEFAULT '[]',
project TEXT DEFAULT '',
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_packages_name
ON packages(name);
CREATE INDEX IF NOT EXISTS idx_packages_project
ON packages(project);
CREATE INDEX IF NOT EXISTS idx_packages_expires_at
ON packages(expires_at);
Column reference¶
id- 8-character lowercase hex string. Primary key. Generated at store time using
crypto/rand. name- The name given by the
--nameflag. Not unique — multiple packages can share the same name. The most recently created package with a given name is returned byretrieve --name. summary- Optional single-line description from
--summary. Defaults to an empty string. Not used for lookup. content- The full package body, stored verbatim as the text piped into
handoff store. tags- Stored as a JSON array string, e.g.
["auth","api","decisions"]. Parsed from the comma-separated--tagsvalue at store time. project- Optional project scoping key from
--project. Defaults to an empty string. created_at/expires_at- UTC datetimes.
expires_atis computed at store time ascreated_at + TTL duration. Both are stored and queried in UTC.
Indexes¶
Three indexes are maintained to keep operations fast regardless of database size:
| Index | Used by |
|---|---|
idx_packages_name |
retrieve --name lookups |
idx_packages_project |
list --project filtering |
idx_packages_expires_at |
Garbage collection (DELETE WHERE expires_at < NOW()) |
Package IDs¶
IDs are generated using Go's crypto/rand package:
- 4 bytes are read from the OS cryptographic random source
- The bytes are hex-encoded, producing an 8-character lowercase string (e.g.
a3f9c12e)
This gives 2³² (approximately 4.3 billion) possible IDs. Given that a single user's local database typically holds a handful of packages at any time, the probability of a collision is negligible.
IDs are assigned at store time and never change.
TTL and garbage collection¶
How TTL works¶
TTL is specified at store time as a duration string and immediately converted to an absolute expires_at UTC timestamp. It cannot be changed after the package is stored.
Supported formats:
| Format | Example | Duration |
|---|---|---|
Nh |
2h |
N hours |
Nd |
14d |
N days |
N must be a positive integer. 0h, -1d, and similar are rejected with an error.
Lazy garbage collection¶
handoff runs no background process. Instead, it garbage-collects expired packages lazily — at the beginning of every database operation:
This runs on every call to store, retrieve, list, and gc. The result is that expired packages are silently removed during normal use, and you will never see an expired package in list output or receive one from retrieve.
Explicit GC¶
Running handoff gc triggers the same deletion and reports how many rows were removed. Use it when you want to confirm cleanup has occurred or reclaim disk space immediately.
Configuration¶
handoff has exactly one configuration surface: an environment variable for the database path.
| Variable | Default (macOS / Linux) | Default (Windows) |
|---|---|---|
HANDOFF_DB |
~/.handoff/handoff.db |
%USERPROFILE%\.handoff\handoff.db |
There is no config file. No other settings exist.
Design principles¶
handoff was deliberately kept minimal. Every design decision reflects one of these principles:
- Zero config. Works out of the box with no setup. The first command you run creates the database automatically.
- Single binary. One executable, no runtime dependencies, no install scripts.
- Cross-platform. Pure Go, no CGO. Identical behaviour on macOS, Linux, and Windows for both amd64 and arm64.
- Stdin/stdout. Content flows through pipes. This is the natural interface for agents working in a terminal, and it makes
handoffcomposable with other tools. - Ephemeral by default. TTL ensures stored context eventually disappears. Old packages do not accumulate forever.
- Simple over clever. No plugin system, no network layer, no authentication, no daemon. A single SQLite file is the entire data layer.