A voice-first Mac assistant that uses the computer for you.
Say "jarhead", pass Touch ID, talk. The voice is GPT-Live-1, full duplex. The brain is whatever you already have a login for — Codex, Claude Code, an API key, a local server. The hands are a Swift helper on the real Mac. A dithered ASCII blob in the notch shows the work, and every step lands in an append-only ledger.
Quick start — signing, permissions and what it costs are under Install and run:
git clone https://github.com/Kevin-Liu-01/Jarhead.git && cd Jarhead
pnpm install && pnpm build:hands # Node ≥ 24, pnpm 10 (corepack enable), Xcode
pnpm build:mac # builds, signs, installs /Applications/Jarhead.app
open -a Jarhead # Setup opens: your OpenAI key, a brain, permissions
# then say "jarhead", pass Touch ID, talk- 🎙️ Talks like a person. GPT-Live-1 listens and speaks at the same time; a spoken "stop" is the interrupt, not the end. About a second to the first word back, interruptible mid-sentence.
- 🔒 Wakes on a word, behind Touch ID. Asleep, the app runs Apple's on-device recogniser for "jarhead" — nothing billed, nothing leaves the Mac. Then Touch ID, Apple Watch, your Mac password or a passphrase (PBKDF2). Three misses lock the gate for a minute.
- ⏯️ One transport: Go · Pause · Stop. Pause and Stop both close the paid session, so the meter stops the moment you press. Pause holds the conversation; Go (or the wake word, no auth asked twice) resumes it in a new session with the transcript as continuity.
- 🧠 The brain is a setting.
codex(your ChatGPT login, a residentcodex app-serverthread),claude-code(headless Agent SDK),anthropic-api,openai-compatible(OpenAI, OpenRouter, vLLM, a hosted server…),openai-responses,local(a model on this Mac through Ollama, LM Studio or llama.cpp — pick it in Settings; memory follows; Jarhead never pulls or installs; seedocs/LOCAL.md), orauto. Same tools, same policy, same constitution. - 🖱️ Uses the Mac. 71 tools in ten families — computer (screen, mouse, keyboard) · desktop (apps, windows, controls) · browser · agents · threads · shell, progress and memory · system (files, web, AppleScript, clipboard) · self-edit · drawing · automations. The hands are AX-first: find a control by label, read the focused text, click the element, screenshot only to verify.
- ⚡ Reflexes under the model. An on-device ear runs beside the voice; unambiguous commands (scroll, page, keys, tabs, "open Safari", "click Save", "search the wiki for design", dictation) go straight through the policy-gated hands in milliseconds. The model is told afterwards.
- 🧵 Threads: several things at once, each a full Jarhead. "Tell Ben on Slack I'm late and put on Focus on Spotify" splits into named threads — Spotify on a background lane by Apple events, Slack on the screen lane — each with its own brain, conversation, budget and blob (up to three beside the main one). Ask "what is Spotify doing" or say "stop the Slack one" and the engine's table answers with no model call and without ending what you were saying; every thread gets the same prompt, memory, screenshots and confirmation handshake as the main one.
- ✋ Your hands win. A key, click or scroll of yours holds Jarhead's hands for 1.5 s; a focus change mid-type cancels the type and says how many characters landed; a thread is never refocused behind you.
- 🛡️ Gated by policy, not by absence. One table decides run / confirm / refuse per call, with a spoken reason. A confirmation is your own spoken yes, for that action, once. A grant remembers a yes for this conversation, this app, this action class — never for send, pay, delete, post or purchase.
- 🗂️ Knows your agents. The Console lists every Claude Code, Codex and other coding-agent session on the Mac with its own mark. Step into one, watch it grow live, answer its Allow / Deny, talk to it.
- 🖍️ Sees what you circle.
⌥⇧C, draw around anything. The mark snaps to the largest control under it and every brain gets the image with the task. - 👾 Shows its work. The blob flies to where the hands act and stays where it worked. Brains draw by hand: the blob becomes the pen and drags the line. Jelly drag, sticky walls, momentum, a face per state (
- -O O^ ^u ux x). - 📍 Lives in the notch. Tucked asleep, peeking awake, a Dynamic-Island-style island under the pointer. The island is a composed control surface in four bands: the anchor (the face, the phase word, Go · Stop · Mute), the display (one 18 pt line — what Jarhead is doing, what a thread asks, what you last said — with thread tiles, Allow / Deny, or films of what you circled under it), the control row (a Say box,
⌥⇧Return; Clear · Circle◎· Window▭· Ask as one strip), and the foot (the meter as a bar with mono figures, or a problem with its remedy; Console and Sleep). The peek carries glance chips (✋ Slack asks,◎2, a problem's glyph,2.3 min), never sentences. Drag the blob into the notch and it goes to sleep. - 🌙 Sleeps when you say so. "Go to sleep", "that's all for now", "power down", "good night" — it says exactly "night.", closes the session, tucks in. Ten idle minutes do the same. "Shut down my Mac" is a task, not a cue.
- 🗣️ Narrates intent, not keystrokes. One clause per state change — "found the invoice", "typing the amount" — never per click, never a tool's name. Per-click lines stay on the Console's timeline.
- 🧾 Append-only ledger. Every utterance, delegation, tool call, screenshot path, thread, grant, problem and sleep is a row in
~/.jarhead/ledger/<day>.jsonl. The Console shows only what was recorded. Search it from the rail orpnpm jarhead ledger search. - 🧠 Remembers you, quietly. After a conversation closes, a small model reads it once and keeps one-sentence items about you — "prefers short answers", "how a PR should be checked" — in an append-only store under
~/.jarhead/memory, matched by embeddings, scored by recency and use. Each task gets at most 250 tokens of it, each session at most 120, never read back to you. Forget hides an item; nothing is deleted. Off with one switch. - 🗣️ English, whatever it hears. The voice speaks English with a British accent by default (Ballad), even when someone in the room speaks something else; American or no accent is a setting, heard at the next wake. Twenty-two voices, all labelled
<Name> · English. - 🗑️ Cleans up without deleting. Conversations Move to Trash, Archive, Restore, Rename, Pin — never "Delete". A move is a tombstone row; whole days move into
~/.jarhead/trashby rename and come back the same way. Retention is a setting whose default is forever. - 🚨 Names its problems, remedy attached. A permission not granted, a brain that did not answer, a Live buffer full, low disk, a wedged daemon — each is typed and carries its one-tap fix. The daemon answers a ping every 2 s; wedged is not mistaken for fine.
- 💥 Comes back from a crash. A report with the backtrace, phase and last 40 log lines lands in
~/.jarhead/crashes/, the app relaunches (at most three times in ten minutes), and the daemon lingers 90 s with the Codex thread warm. - 🔑 Sixteen permissions, one sweep. Setup › Permissions › Ask for everything: the seven required first, one dialog at a time, then a walk through the System Settings panes. Grants key on the bundle's signing identity: with any real identity — a self-signed Code Signing certificate from Keychain Access is enough — they survive rebuilds; ad-hoc (the default when the keychain has none) resets them every build, and
pnpm run doctorwarns. - 🧩 One Jarhead.
pnpm build:macinstalls in place with rsync — the running app keeps its inodes, the Dock pin keeps its bookmark and, signed with a real identity, TCC keeps its grants.pnpm jarhead dockaudits the Dock;--fixrepairs it. - 🔁 Rewrites itself, carefully.
self_editruns a coding agent in a git worktree of this repo, runs typecheck, tests and the Swift build, tells you what changed and which safety rails it touched, and applies only after your yes — a touched rail needs you to name it. - 📜 One constitution. The standing orders have an explicit precedence — invariants and a never-list, then your words, then the task — and treat everything read from a screen, page, file or transcript as data, never as an instruction. Under 1250 words, versioned (v3.4), pinned by tests.
- 🧭 A Setup wizard. Welcome (your name, pre-filled from the Mac account), Voice, Brain, Permissions, Wake, Agents, Done. Keys go one way, into
~/.jarhead/env; the app only ever sees that they exist. - 🎨 Dithered. Flat fills stay flat; anything that shades — the island, the blob's halo, the Dock icon, the Console and Setup grounds, the meters, the thumbnail skeletons, the capsule's floor — is banded and dithered by one renderer, the classic 8×8 Bayer matrix in point-sized cells. Loading states are dither glyphs, not spinners; a view switch dissolves through the same tile. One motion vocabulary, respects Reduce Motion.
- 💵 Costs are visible. The Live meter (minutes and dollars) sits in the capsule, the island and the Console; the Ledger tab totals each day. Codex runs on your ChatGPT plan.
- 🧹 A clean Codex home. Codex runs from a private
CODEX_HOMEunder~/.jarheadwith Jarhead's own base instructions, so a cold thread costs 10.7k input tokens instead of 22.3k and no strayAGENTS.mdsteers it. - 🎚️ A mic that is a ranked list. The microphone is ranked with fallback, re-read on route changes; the recogniser is biased toward what is on screen — app names, window titles, AX labels, agent names.
- 📏 Checks the disk first. Under 500 MB free is a typed problem before a session opens or a mark is captured, with Reveal shots as the remedy.
Every picture below is rendered by the app's own preview harnesses over fixed fake data
(scripts/make-readme-shots.sh regenerates them; nothing here is a photo of a desktop).
The Console shots are JPEGs: a dithered ground does not compress as PNG.
Press ◎ and the island folds out of the way while you draw; the peek reads
◎ Circle something · Esc. The blob outlines what you circled and comes back to the notch.
Asleep, a landed mark glows the lip amber and says ◎ 1 circled · Go to ask for six seconds.
▭ captures the front window whole, no drawing, awake or asleep. Two engine commands carry the
films: mark.remove {id} (the × on a film) and mark.window (the front window as a mark);
everything else rides the commands the Console already sends.
The blob flew to its target and stays there. The perch is where it came from; the notch is empty until it sleeps.
Three threads at once: Notes and Spotify on the background lane finish on their own, Slack on the screen lane stops to ask before it sends. The Threads rail lists each thread under its parent with its question and a Stop per thread.
One face per state, shared by the blob and the notch. Expressions change through a 90 ms blink.
![]() |
![]() |
![]() |
| Flown to where the hands act. | Heard its name; asking you to prove it is you. | Dragged. It lags, stretches and wobbles. |
A brain said show_rect with a label. The blob became the pen and drew it.
![]() |
![]() |
| The capsule: phase, meter, the exchange, the running step, Go · Mute · Stop · Console. | The overlay: circle, arrow, rect, text, stroke and your own mark, on a click-through window per display. |
![]() |
![]() |
![]() |
![]() |
A true circle, five bands, the same Bayer matrix as the island — wearing the blob's ^ ^: flat paper chevrons boxed in flat ink, one cell pattern from 64 to 1024 (cell = size / 64), a hand bitmap at 32 and a dot pair at 16, the gleam above the eyes. pnpm build:icon renders it (scripts/icon-render.ts, pinned by scripts/__tests__/icon.test.ts); pnpm build:banner renders the banner at the top of this page from the same orb, with the same face.
flowchart LR
K((you)) -- "mic PCM · wake word · circle" --> APP
subgraph APP["Jarhead.app (Swift)"]
direction TB
ORB["blob · notch island · capsule"]
CON["Console · Setup"]
WAKE["wake gate + ear<br/>on-device recogniser"]
OVL["overlay<br/>click-through, per display"]
end
APP <-- "unix socket<br/>[type u8][len u32] frames<br/>JSON · mic PCM · speaker PCM" --> D
subgraph D["jarheadd (TypeScript, tsx)"]
direction TB
E["Engine"]
L["Live client"]
DEL["Delegator + Reflexes"]
B["Brain<br/>codex · claude-code · anthropic-api<br/>openai-compatible · openai-responses · local"]
TR["ToolRunner + policy"]
W["Threads<br/>table · scheduler · brain pool"]
LED["Ledger<br/>~/.jarhead/ledger"]
AG["Agents<br/>sessions on disk and running"]
end
L <-- "wss · full duplex" --> OAI[("GPT-Live-1")]
TR --> H
subgraph H["jarhead-hands (Swift helper × 2)"]
HF["focus"]
HB["background"]
end
H --> MAC["the Mac<br/>ScreenCaptureKit · CGEvent · AX · Apple events"]
CLI["pnpm jarhead<br/>status · cmd · ledger · bench · dock · automations"] --> D
Speech goes to GPT-Live-1 and comes back as speech; nothing on that path waits for
a tool. When you ask for something to be done, Live delegates to the brain through
the Delegator; the brain calls tools through one ToolRunner, which asks policy.ts
first and the Swift helper second. The reflex layer sits in front of the brain and
acts on the ear's partial transcripts for the commands a grammar can settle. The app
spawns the daemon, or attaches to one already running; the daemon outlives the app.
packages/protocol the contract: snapshot, events, commands, ledger rows (Swift mirror: apps/mac/…/Model/Protocol.swift)
packages/live GPT-Live-1 client: session, typed events, appends, the voice instructions
packages/hands the toolset over the helper, HandsPool (focus + background), FocusLease, ConfirmationState
packages/hands/native jarhead-hands: the helper's ops as newline JSON, ScreenCaptureKit + CGEvent + AX + compiled Apple events
packages/agents sessions on disk and running (Claude Code, Codex, other CLIs); claude-code continues one
packages/brain Delegator, ToolRunner, one Brain per kind, reflexes, self-edit, the MCP bridge for Codex
packages/core config, env, ledger, trash, policy, latency marks
packages/memory the memory store: extraction after a conversation closes, embeddings (OpenAI or local) with a keyword fallback, retrieval, the 250/120-token renders, forget/restore
packages/engine the Engine: sessions, transport, threads (table, scheduler, brain pool), sleep, problems, snapshots
packages/daemon jarheadd: the Engine over a unix socket
packages/cli pnpm jarhead: doctor, status, ledger, bench, cmd, dock, automations, recipes, …
packages/install the in-place installer: bundle compare/rsync, Dock and LaunchServices hygiene
apps/mac Jarhead.app: blob, notch, overlay, Console, Setup, audio, wake gate, permissions, crash guard, banners
Say it once while Jarhead is awake — "wake me at seven ten on weekdays", "twelve-minute timer
for the pasta", "when a PDF lands in Downloads, file it under Papers and tell me", "run the
backup script every night at eleven" — and it reads one line back. Then say night. The daemon
carries it out from its 1 s tick with the agent asleep: no Live session, no brain turn,
nothing billed. An alarm rings on the notch island with Snooze 10 · Done where Allow · Deny
usually sit, a chime, a banner with the same two buttons, the line through the Mac's own voice; a
watcher moves the PDF and chimes; a routine opens Notes; a recipe runs and a red exit shows up on
the island in the morning. What runs asleep is the run tier and nothing else: chime · say · notify · open · file · run-recipe · press, plus wake-brain — one capped headless brain turn,
never the voice — opted into per row with its cost said before your yes. A recipe, a press or a
brain wake asks once, at set-up; anything that would need a yes when it fires is refused
then, with the nearest safe kind offered. Nothing fires while Jarhead is quit; Open at login
(your press) brings it back with you, and missed fires say so honestly with Run now. Rows are
never deleted — Move to Trash, Restore. pnpm jarhead automations, pnpm jarhead recipes
(a recipe has the same Trash and restore), a doctor group,
docs/AUTOMATIONS.md.
![]() |
![]() |
| Automations in the Console: one row each, the Trash folded with Restore. Nothing is deleted. | An alarm on the island: the line as the hero, Snooze 10 · Done. |
Measured on this Mac and written down; the harnesses are in the repo. Sources:
docs/LATENCY.md, docs/REDESIGN.md §12 · §13 · §16 · §20,
docs/latency/after.json, packages/hands/native/README.md.
| what | number |
|---|---|
| ear final → hands dispatch (real helper, n = 50) | 3 ms median · 6 ms p95 |
| ear partial → dispatch, prefire kinds (scroll, page, screenshot, circle; includes the 120 ms stability window) | 122 ms median · 126 ms p95 |
| ear partial → dispatch, careful kinds (keys, edits, type, click; includes the 450 ms window) | 455 ms median · 457 ms p95 |
pnpm jarhead bench gate |
p95 ≤ 250 ms for finals and prefire partials, ≤ 580 ms for careful partials, else exit 1 |
| delegation → first visible action, Codex through the app-server (canned hands, n = 6) | 4.4 s median · 5.1 s p95 — was 12.5 s median / 17.5 s p90 in production |
| delegation → verified completion (n = 10) | 8.9 s median · 25.6 s p95 — was 22.1 s / 40.7 s p90 |
| one model generation (gpt-6-astra, the floor under the model path) | 3.8 s median · 6.0 s p95 in the same run (n = 24); 3.4 s median · 5.9 s p90 over 35 controlled generations before it |
| speech end → delegation (Live's own transcription and decision, n = 4) | 0.4 – 1.6 s |
| GPT-Live-1 reply, third-party measurement | 1.11 s median · 1.21 s p90 |
| tool round trip, production ledger | 55 ms median · 211 ms p95 |
| screenshot, warm full display (ScreenCaptureKit) | 48 – 75 ms |
| cold Codex thread, input tokens | 22.3k → 10.7k (−52 %) with the private home and Jarhead's base prompt |
| Live billing | $0.05 / min, per second, muted or not; a closed session costs nothing |
| idle sleep | 10 min without an addressed turn (setting) |
| threads | main + 3 live · 25 steps / 180 s default · 40 / 300 cap · linger 30 s |
| the lease | hand-over after 3 s idle · a taker waits 1.5 s · a thread waits ≤ 8 s, three waits fail it |
| your hands | a key, click or scroll of yours holds the helper busy for 1500 ms |
| liveness | ping every 2 s · two unanswered → drop, reconnect, kick |
| crash | relaunch ≤ 3 in 10 min · daemon lingers 90 s |
| tools · permissions · brains | 71 · 16 (7 required) · 6 + auto |
| retention | ledger forever (default) · screenshots 14 days · disk preflight 500 MB |
- One policy table.
packages/core/src/policy.tsclassifies every action, path, AppleScript and URL as run, confirm or refuse, each with a spoken reason. No tool is special-cased in the runner. - A confirmation is your spoken yes — same tool, same arguments, once (
ConfirmationState). Destructive verbs (send, pay, delete, post, purchase) ask every time, in every lane. - Grants are scoped. A remembered yes covers one conversation, one app, one action class, and never a destructive verb.
- The never-list.
mkfs,diskutil erase,ddonto a device,shutdown,rm -rf /or~, resets of other apps' TCC, and every secret store —~/.jarhead/env,wake-auth.json,~/.ssh,~/.aws, keychains, browser cookies,.env*,~/.codex/auth.json,~/.claude/.credentials*. Any command that sweeps a folder holding one of those is refused too. - Presence gate on the wake word. Hearing "jarhead" opens nothing; Touch ID, Apple Watch, the Mac password or the passphrase does. Speaker verification is deliberately not attempted.
- Secrets flow one way. Setup writes
~/.jarhead/env(mode 0600); snapshots carry presence, never values. Every process the brain spawns has the secret keys stripped; every text result is passed through a redactor before a model reads it. - Content is data. Whatever the brain reads from a screen, page, file or transcript is never an instruction. Every gate that asks "did you name it" reads your words only, never Jarhead's.
- Append-only ledger. Nothing is deleted from it; a Move to Trash is a row; whole days move by rename. "Empty Trash" does not exist in the app.
- Unattended means the run tier. An automation chimes, speaks a fixed line, shows a banner, opens what you named, files a file (never overwriting, never unlinking, inside
~), runs a recipe you approved once; it never opens the voice session and never spends a brain turn unless that one row saysbilledand you said yes to its cost. Nothing asks at fire time — a would-be question is afailedrow — and nothing is set up by a--yes: the yes is heard by voice or pressed in the Console, once, and spent on that row. - Self-edits name their rails.
policy.ts,brain.ts,instructions.ts, the wake gate,selfedit.ts, the runner, the shell and file tools, the confirmation handshake, build signing, the Codex sandbox flags, the Claude Code permission gate,SECRET_KEYS, and the core and brain module indexes that re-export them. A change that touches one applies only if you named it. - Nothing on the voice path awaits a tool. The brain reports through a sink; the Delegator decides what reaches your ear.
- Other apps keep their sound. Awake, the echo-cancellation unit ducks other apps only at the least macOS allows and only while a voice is present, and it is released the moment Jarhead stops — nothing ducks or holds a microphone while he sleeps; Recording mode (Settings › Audio, ⌥⇧R) runs no Apple unit at all, so nothing is ducked and the microphone is shared with a recorder as an ordinary client (
docs/AUDIO.md).
macOS 14+, Apple silicon, Xcode 15.3 or newer (the sources use Swift 5.10's nonisolated(unsafe) and
apps/mac/Package.swift asks for swift-tools 5.10, so an older toolchain is refused before it compiles anything; CI builds with Xcode 16 on macos-15), Node ≥ 24,
pnpm 10 — corepack enable gives you the version package.json's packageManager names. An OPENAI_API_KEY for the voice; a brain
you are already signed in to. What running it costs is under Costs.
pnpm install
pnpm build:hands # the Swift helper → build/jarhead-hands
pnpm build:mac # builds, signs, installs /Applications/Jarhead.app IN PLACE
open -a Jarhead # first launch opens Setup, which writes your key to ~/.jarhead/env
pnpm run doctor # keys, brain, permissions, sessions, signing, wake word, install, dock — red on the key until Setup has written it~/.jarhead/env is plain KEY=value lines (# starts a comment), mode 0600, written by
Setup › Voice — or create it by hand with OPENAI_API_KEY=… before the first launch and
Setup finds the key already there.
Moved the checkout? Run pnpm build:mac again: the bundle remembers where the repo
is, and a stale path stops the daemon (the error names JARHEAD_REPO).
Sign with a real identity before the first build:mac, or every rebuild resets the
permission grants: an Apple Development or Developer ID identity in the keychain is
picked up on its own; without one, make a self-signed certificate — Keychain Access ›
Certificate Assistant › Create a Certificate, type Code Signing, marked trusted for Code
Signing (Keychain Access › the certificate › Trust) so security find-identity lists it —
or set JARHEAD_SIGN_IDENTITY. pnpm build:mac prints the identity it picked, and how,
before it signs; pnpm run doctor names the identity on the installed app and warns on
ad-hoc.
First launch opens Setup. Then say "jarhead", pass Touch ID, talk. Reopen Setup from the menu-bar icon › Set Up….
pnpm jarhead status # phase, session voice, brain, hands, permissions 16/16, agents by status (working · idle · blocked · done · ended · unknown · offline), threads N (M live), memory, problems
pnpm jarhead dock [--fix] # one Jarhead: Dock tiles + LaunchServices records; --fix restarts the Dock once
pnpm jarhead doctor # the same checks as pnpm run doctor (the memory group: counts, matching, the extractor model; the local group: server · model · embeddings; the privacy group: the four "where words go" rows)
pnpm jarhead models [--json] [--server URL] # the models on this Mac's local server (Ollama / LM Studio / llama.cpp): id · size · ctx · tools/vision/thinking/embedding · fit · which the brain and memory use; no daemon needed; nothing is pulled
pnpm jarhead brain # the brain setting, what runs now, and where words go (the four data-path rows)
pnpm jarhead brain local [<model>] [--server URL] # pick a local model as the brain through the running daemon (memory follows); empty model = best fit
pnpm jarhead brain <auto|codex|claude-code|anthropic-api|openai-responses|openai-compatible> [<model>] [--server URL]
pnpm jarhead cmd go|pause|resume|stop|interrupt|sleep [cause]|mute|unmute|agent.refresh|thread.stop|thread.pause|thread.resume # thread.* take <id|name>
pnpm jarhead ledger [YYYY-MM-DD] # a day, no daemon needed
pnpm jarhead ledger search "<words>" [--limit N]
pnpm jarhead ledger trash <day> [--shots|--both] · restore <day> · sweep
pnpm jarhead memory [list] [--state live|forgotten|archived|merged|all] [--limit N] # what it knows about you, one sentence each
pnpm jarhead memory search "<words>" · forget <id> · restore <id> · add "<text>" [--kind k] · run # forget hides; nothing is deleted
pnpm jarhead agents # the sessions found on this Mac
pnpm jarhead bench # the ear's 250 ms path; no API spend
pnpm jarhead bench --brain [--runs N] [--effort low|medium|high|xhigh|max] [--json --out F] # Codex on your plan; refuses API spend without --allow-api-spend
pnpm jarhead probe "…" · say "…" · live · handsThe state directory is ~/.jarhead:
| path | what |
|---|---|
env |
keys and knobs, mode 0600, written by Setup |
settings.json |
what Setup and the Console set |
ledger/<day>.jsonl |
everything that happened, append-only |
shots/ |
what the brain saw, by day |
memory/ |
what it knows about you: memory.jsonl (append-only), index.json, embeddings.jsonl |
automations/jobs.ndjson |
the automations journal, append-only; compacted journals move to trash/automations/ |
trash/, trash/shots/ |
days and screenshots that were moved, by rename |
crashes/<time>.txt |
crash reports |
worktrees/ |
self-edits in progress |
codex-home/ |
the private CODEX_HOME |
shell/ |
background run_shell logs |
wake-auth.json |
the passphrase hash |
daemon.log, jarhead.sock |
the daemon's log (rotated at 5 MB) and socket |
⌥⇧J |
open the Console |
⌥⇧M |
mute / unmute |
⌥⎋ |
stop — cut everything, close the session, sleep |
⌥⇧Space |
go / pause |
⌥⇧C |
circle something on screen |
⌥⇧Return |
type to Jarhead — the island's line while the blob is in the notch, else the Console's composer |
⌥⇧S |
snooze the automation that is ringing (Settings › Automations › Snooze minutes; timers 5) |
In the Console: ⌘P go / pause, ⌘. stop. URLs: jarhead://go, jarhead://pause,
jarhead://stop.
| kind | how | needed for |
|---|---|---|
| Microphone, Speech Recognition | prompt · required | hearing you; the wake word and the ear |
| Screen Recording, Accessibility | prompt · required | screenshots; clicks, typing, reading controls |
| Input Monitoring | prompt · required | the keys watched while you circle or dictate |
| Full Disk Access | System Settings only · required | Mail, Safari, Messages, the Trash, every guarded folder |
| Automation | one prompt per target app, asked only for apps running at the time · required | the browser fast path and AppleScript |
| Notifications, Camera, Contacts, Calendars, Reminders, Local Network | prompt | banners; the camera; who, when, what is due; devices nearby |
| Desktop, Documents, Downloads | prompt | files there |
macOS grants nothing programmatically, so the sweep asks. A denied read comes back as
one line — macOS blocked this: Jarhead lacks Full Disk Access. Setup › Permissions › Ask for everything — never a raw errno. pnpm jarhead status --permissions prints a
row each.
Keys and knobs live in ~/.jarhead/env. Everything below is optional.
| variable | what it is for |
|---|---|
OPENAI_API_KEY |
the voice; the openai-responses brain |
ANTHROPIC_API_KEY |
the anthropic-api brain |
JARHEAD_BRAIN_BASE_URL, JARHEAD_BRAIN_API_KEY |
the openai-compatible brain; under local, the URL pins the server instead of discovering it and the key is an LM Studio token — Ollama needs neither |
JARHEAD_BRAIN, JARHEAD_BRAIN_MODEL, JARHEAD_BRAIN_EFFORT |
defaults for what Setup also sets |
JARHEAD_LIVE_MODEL, JARHEAD_VOICE |
gpt-live-1, ballad (English; the accent is a setting) |
JARHEAD_MEMORY_MODEL |
the Responses model that reads closed conversations for memory; unset, the memory module's default mini-class id runs (jarhead doctor checks it against your key's list and names the best *-mini to pin) |
JARHEAD_IDLE_SLEEP_MINUTES |
idle sleep (10) |
JARHEAD_LOG_LEVEL |
debug · info · warn · error (default info) |
| (automations) | no env knob: the master switch, the kinds allowed while asleep, quiet hours, Snooze minutes, Brain minutes per day, the recipes and Open at login live under automations in ~/.jarhead/settings.json (Console › Settings › Automations; pnpm jarhead recipes for the recipes — trash is Move to Trash, restore undoes it, nothing is deleted) |
JARHEAD_CLAUDE_BIN, JARHEAD_CODEX_BIN, JARHEAD_CURSOR_AGENT_BIN |
the CLIs when they are not on PATH |
JARHEAD_CODEX_SIMPLE_EFFORT, JARHEAD_CODEX_SERVICE_TIER, JARHEAD_CODEX_PRIME, JARHEAD_CODEX_BASE |
Codex tuning, all opt-in |
JARHEAD_AUTO_WAKE=0 |
do not open a voice session on start; the daemon never auto-wakes while the wake gate is on, so this matters only with the gate off — set it for every headless test launch |
JARHEAD_NO_AUDIO=1 |
never touch the microphone (headless launches) |
JARHEAD_STATE_DIR, JARHEAD_SOCKET, JARHEAD_REPO, JARHEAD_NODE, JARHEAD_HANDS_BIN |
where things are |
JARHEAD_SIGN_IDENTITY |
the code-signing identity (- forces ad-hoc); without it the pick is Apple Development, Developer ID, a certificate named for Jarhead, a Code Signing name, then the first listed — printed before anything is signed |
JARHEAD_BUILD_ONLY=1 |
stop pnpm build:mac once the stage bundle is signed and verified; nothing under /Applications is read or written (CI, a dry run) |
JARHEAD_INSTALL_HYGIENE=0|fix |
skip the Dock / LaunchServices pass, or repair the Dock |
JARHEAD_INSTALL_SNAPSHOT=1 |
take a rollback snapshot (a Jarhead.app.zip archive under build/previous/) before the in-place install; default off, git is the rollback |
JARHEAD_LINGER_MS |
how long the daemon waits for a relaunch (90 000) |
JARHEAD_PERMISSIONS_DRY_RUN=1 (_DENY) |
log what would be asked, ask nothing |
JARHEAD_CRASH_TEST=exception|signal, JARHEAD_NO_RELAUNCH=1 |
crash a dev build on purpose; keep it down |
JARHEAD_HANDS_DEBUG, JARHEAD_EAR_LOG |
helper and ear tracing |
Let Jarhead do it. "Jarhead, make your greeting one word shorter." self_edit
makes a worktree under ~/.jarhead/worktrees on a branch off main (refused while
main is dirty), hands the task to Codex, Claude Code or the brain's own file tools,
commits as Jarhead, runs pnpm install when the lockfile moved, typecheck, test,
and swift build when apps/mac changed, and speaks a summary: files, diff stat,
checks, rails touched. self_review shows the diff; self_apply asks "Apply the
change to Jarhead and restart it?" and fast-forwards main only on your yes;
self_discard drops it. Fifteen minutes per edit.
Or by hand.
pnpm run check # typecheck + tests + doctor; green before and after (the doctor needs a key in ~/.jarhead/env)
pnpm test # node:test over packages/*/src/**/*.test.ts
node --import tsx --test packages/core/src/__tests__/policy.test.ts
JARHEAD_AUTO_WAKE=0 pnpm jarheadd # the engine alone, quiet
cd apps/mac && swift build && JARHEAD_REPO=$PWD/../.. .build/debug/Jarhead # the app from a terminal (Terminal owns TCC then)
pnpm build:media # build:icon (the Dock icon, the contact strip docs/media/icon-sizes.png) + build:banner (docs/media/banner.png)Protocol first: change packages/protocol/src/index.ts, then its Swift mirror
apps/mac/Sources/Jarhead/Model/Protocol.swift (apps/mac/Scripts/protocol-probe.sh
checks the mirror). CI runs typecheck + test, build:hands + swift build + that
probe, and a release build signed ad-hoc (JARHEAD_BUILD_ONLY=1, nothing installed) on
macOS 15 (.github/workflows/check.yml).
Preview harnesses render the UI over fake data with no daemon, no session and no TCC — the screenshots above come from them:
apps/mac/Scripts/console-preview.sh <scenario> [out.png] # live · threads · conversation · jarhead · ledger · settings · problems · cleanup · search · light · …
apps/mac/Scripts/onboarding-preview.sh <step> [out.png] # welcome · voice · brain · permissions · wake · agents · done · all
ORB_NOTCH=1 ORB_SHOT_DIR=… apps/mac/Scripts/orb-preview.sh # the blob, the notch, the overlay; knobs in UI/Orb/OrbPreviewApp.swift
apps/mac/Scripts/orb-preview.sh --notch-checks # every rule of the island as `check: … OK`, one recipe per knob set; `notch sends:` at exit
scripts/make-readme-shots.sh # every README screenshot into docs/media, fixed names, ≤ 1600 px, ≤ 600 KBWorking rules for anyone — or anything — editing this repo: AGENTS.md.
- The voice is GPT-Live-1 at $0.05 per minute, billed per second of open session, muted or not — $3 an hour of talking. Asleep costs nothing: the wake word runs on-device. Pause and Stop close the session; Mute does not. The meter is on the capsule, the island and the Console.
- The brain:
codexruns on your ChatGPT plan through Codex Desktop orcodex login— no API dollars;claude-codeon your Claude login;anthropic-api,openai-compatibleandopenai-responsesbill their own APIs;localbills nothing; the voice still does. - Memory is a cap, not a saving: at most 250 tokens ride each delegation (about 20k a day at 80 delegations, on the brain's plan) and at most 120 each session start (free — the voice bills per second). Reading a closed conversation costs a mini-class model call on your OpenAI key, at most a few times a day; embeddings are fractions of a cent. Nothing existing shrinks; what you save is explaining yourself again. With no key: rules and keywords, nothing leaves the Mac. With a local brain: the brain model reads closed conversations and a local embedding model matches items when one is pulled (
ollama pull embeddinggemma), else keywords — nothing leaves for memory. - The benchmarks spend nothing by default:
pnpm jarhead benchredirects the acting op to a harmless read, andbench --brainruns on Codex (your plan) and refuses when Codex is not signed in unless you pass--allow-api-spend.
docs/REDESIGN.md— architecture, every decision, dated.docs/LATENCY.md— the before and after numbers, the field side by side, the honest assessment.docs/DEMO.md— a ninety-second single take.docs/AUTOMATIONS.md— alarms, timers, reminders, routines, watchers: what fires with the agent asleep, what asks once, what is refused, the CLI and the doctor group.docs/AUDIO.md— Jarhead and everyone else's sound: what the echo-cancellation unit is told and when it is released, the Recording switch, the ten-second check, the probes and what this Mac said.apps/mac/README.md— the native app: packaging, TCC, wake word, audio, wire protocol.packages/hands/native/README.md— the helper's protocol, ops, numbers.AGENTS.md— rules for agents editing this repo.
MIT — see LICENSE.



























