Skip to content

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.

harn presence get # print just the state ("mobile" / "office")
harn presence get --json # full record: state, updated_at, source, is_default, path
harn presence set <state> # set state explicitly (source=cli); <state> = mobile | office
harn presence clear # delete the file (next read returns the default, "office")
harn presence detect # internal: infer state from prompt text on stdin (hook-driven)
Flag Applies to Description
--json get Print the full record as JSON instead of the bare state string.

Two sources write the file, and the source field records which:

  • cli: an explicit harn presence set mobile / office. Deliberate and authoritative for that moment, but not sticky: a later detection can still overwrite it.
  • hook: harn presence detect reads 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.

Terminal window
# 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 session
harn presence set mobile
# Inspect the full record, including how stale it is
harn 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 office rather than erroring.

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 now
harn presence peers [--json] # sessions on other machines (refs + relay cache merged) + daemon status
harn presence mail [--sync] # cross-machine messages: outbox awaiting acknowledgement, counts
harn 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)

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.

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, or configured: 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. truncated means a size limit stopped it early, and history_complete: false means the counts do not cover the daemon’s whole life.
  • relay_cache and git_refs: the other machines this clone has heard from over each transport, with agent counts, publish times, ages and a stale flag. 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.

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.