Skip to content

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.

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.
  • hookharn 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 — 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 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 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 — 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.