harn presence
harn presence [get|set|clear|publish|fetch|peers] [options]Two related surfaces share this command: mobile/office state (is the human at a desktop or on a phone?) and cross-machine session presence (which sessions are live on the other machines that share this repo?). The first half of this page covers mobile/office; cross-machine presence is below.
Records a single bit of context an agent can adapt to: is the human at a
desktop (office) or on a phone (mobile)? On mobile the human can’t see
files, editor state, or local paths — only the chat thread — so an agent that
reads presence can shorten replies and hand over links instead of file paths.
State lives in one small JSON file at ~/.claude/presence; the default when the
file is absent is office.
Run bare, harn presence prints the full record (state, source, age). The
subcommands read, set, and clear that record.
Subcommands
Section titled “Subcommands”harn presence get # print just the state ("mobile" / "office")harn presence get --json # full record: state, updated_at, source, is_default, pathharn presence set <state> # set state explicitly (source=cli); <state> = mobile | officeharn presence clear # delete the file (next read returns the default, "office")harn presence detect # internal: infer state from prompt text on stdin (hook-driven)Options
Section titled “Options”| Flag | Applies to | Description |
|---|---|---|
--json |
get |
Print the full record as JSON instead of the bare state string. |
How the state gets set
Section titled “How the state gets set”Two sources write the file, and the source field records which:
cli— an explicitharn presence set mobile/office. Deliberate and authoritative for that moment, but not sticky: a later detection can still overwrite it.hook—harn presence detectreads a prompt on stdin and infers the state. It’s meant to be wired into a prompt-submit hook so presence tracks the human automatically. Detection only writes when the signal is unambiguous; a mixed or absent signal leaves the current state untouched.
The inference is a small set of first-match-wins rules over the prompt text
(fenced code blocks and quoted lines are stripped first so pasted content can’t
skew it): curly “smart” quotes alone read as mobile (phone keyboards produce
them), straight quotes alone read as office, both together are ambiguous and
skipped, and a short message ending in a trailing space reads as mobile.
Examples
Section titled “Examples”# What state am I in right now?harn presence get
# Branch on it in a script[ "$(harn presence get)" = "mobile" ] && echo "keep the reply short"
# Force mobile for this sessionharn presence set mobile
# Inspect the full record, including how stale it isharn presence get --json
# Reset to the default (office)harn presence clear- The file path (
~/.claude/presence) is a Claude Code convention, but the read/set/clear logic is generic — any agent harness can adopt the same file. - Writes are atomic (temp file + rename), so a concurrent read never sees a half-written record.
- A malformed or unreadable file falls back to the default
officerather than erroring.
Cross-machine session presence
Section titled “Cross-machine session presence”When more than one machine works the same repo (two people, or one person on
two computers), each clone’s coordination layer is otherwise blind to the
others. The presence transport closes that gap with zero configuration:
each machine publishes a small JSON blob describing its live sessions (agent
names, tasks, files held) as a parentless commit force-pushed to
refs/harnery/presence/<machine> on the repo’s own origin, and fetches the
other machines’ refs on a throttled hook cadence. Remote sessions then appear
in agents list, the agents status peers line, and the per-prompt peer
table, labeled @<machine> — advisory only (they don’t block local file
claims). Repo push/fetch access is the only credential; there is no service.
The hooks drive everything automatically (publish on session start/end and turn stop, change-batched with a ~5-minute keepalive; fetch on prompt submit, throttled to ~60s). The subcommands exist for inspection and force-runs:
harn presence publish [--force] # publish this machine's sessions now (synchronous, errors report)harn presence fetch [--force] # pull peer machines' refs nowharn presence peers [--json] # sessions on other machines (refs + relay cache merged) + daemon statusharn presence relay-daemon # run the relay client daemon in the foreground (hooks spawn it detached)The live upgrade: a relay
Section titled “The live upgrade: a relay”Git refs give minute-scale latency. For seconds-scale, configure a relay in
.harnery/config.jsonc:
{ "presence": { "relay": "wss://relay.harnery.com" } }With a relay set, the hooks keep a small per-machine daemon alive that holds a
WebSocket to the relay, publishes on every heartbeat change, and caches
received peer blobs locally — presence peers and all the render surfaces
merge both transports, freshest per machine. The relay sees only opaque room
ids and end-to-end-encrypted payloads (room id and key derive from the repo
identity — root-commit SHA + origin URL + optional committed
.harnery/presence-salt), so the public relay learns nothing about your
sessions. The daemon exits when the machine goes idle and reconnects with
backoff; if the relay is unreachable, presence silently rides the git-refs
floor. Self-hosting: harn relay serve or deploy
relay/worker/ to your own Cloudflare account.
Machine identity comes from the standard label resolution (HARNERY_MACHINE
env → ~/.config/harnery/machine file → hostname). A blob older than ~15
minutes renders as offline and disappears.
Opt out per repo with .harnery/config.jsonc:
{ "presence": { "enabled": false } }or per process with HARNERY_PRESENCE=0. Everything is fail-silent: no origin
remote, no network, or a host that rejects custom refs simply means no remote
peers — never a broken hook.
Design + rationale (including the planned optional live relay): decision record 0016.