harn presence
harn presence [get|set|clear|publish|fetch|peers|mail|relay-status] [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, so any agent adapter 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 mail [--sync] # cross-machine messages: outbox awaiting acknowledgement, countsharn presence relay-status [--json] # diagnose the relay transport from local state (no network)harn 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. Both presence peers and all the render surfaces
merge the two 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.
Checking the relay
Section titled “Checking the relay”harn presence relay-status answers “is the relay working on this machine?”
from local files alone; it makes no network calls. It prints one JSON document
(or a short summary on a terminal without --json) with:
relay: the configured relay URL, orconfigured: false.room: the first 8 characters of the opaque room id, so two machines can confirm they derived the same room. The room key is never shown.daemon: the pid file (.harnery/presence/relay-daemon.json), whether that process is alive, its start time and uptime, and whether its relay URL still matches the config.log: the latest connect, disconnect (with close code), idle exit, takeover and error from the presence-relay log, plus connect and disconnect counts since the running daemon started. It reads the log newest first, including older sealed files, until it has passed the daemon’s start.truncatedmeans a size limit stopped it early, andhistory_complete: falsemeans the counts do not cover the daemon’s whole life.relay_cacheandgit_refs: the other machines this clone has heard from over each transport, with agent counts, publish times, ages and astaleflag. Task text and file lists are left out.publish_state: when this machine last published its git presence ref.
The daemon logs every drop as a relay.disconnected record carrying the close
code, a shortened close reason, whether the close was clean, how long the
socket was up, any socket error, and the reconnect delay it chose. Backoff
resets only after a connection stays up for 30 seconds, so a relay that accepts
and immediately drops the socket keeps backing off. While attempts keep
failing, the daemon logs the first three, then only every power-of-two attempt
or once per 15 quiet minutes, and the next record says how many it skipped.
Cross-machine messages
Section titled “Cross-machine messages”agents ping reaches an agent on another machine through the same origin
(ADR 0222). When the name is
not known locally but appears in another machine’s presence, the message is
queued in this machine’s own mail ref, refs/harnery/mail/<machine>, and
pushed immediately. The recipient machine imports it into the ordinary inbox
on its next prompt after a presence fetch, so a live agent typically sees it
within one to two minutes and a dormant one when that task resumes. The
recipient acknowledges it in its own mail ref; the sender then drops it, and
the acknowledgement is retired once the sender’s ref stops listing it. Message
ids survive the trip, so a repeated import never duplicates a message. A
message nobody acknowledges expires after 14 days.
Each machine writes only its own mail ref, so force-pushes never race. A
machine that has never sent or received cross-machine mail never creates one.
presence mail lists messages still waiting for acknowledgement; --sync
fetches, imports, and publishes in one synchronous step. The relay does not
carry mail: its per-sender cache keeps only the latest frame, which suits
presence but would lose queued messages.
Mail bodies sit in the repo’s refs in plain text. Anyone who can read the repo can read them, and anyone who can push to it can publish a mail ref, the same trust boundary as presence and as the code itself. Keep secrets out of pings.
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, not a broken hook.
Design + rationale (including the planned optional live relay): decision record 0016.