
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
@paigy/harness
Advanced tools
Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.
Runs Claude Code and Codex on your machine, mirrors every turn to your Paigy inbox, and — when you ask it to — rings your phone when an agent is blocked (#804).
Hooks already cover turn capture — .claude/settings.json, .codex/hooks.json, and
paigy-listen + PAIGY_ON_WAKE (#486) between them can observe and wake any harness
without a desktop app. Two things need a process that actually owns the agent:
contact fires because a model chose to call
it. A turn boundary in a harness is a fact of the runtime.The point isn't notifications, it's a conversation with an agent that happens to be
running on your laptop. Both directions close (paigy/conversation.ts):
contact — its only channel to you; the
harness never relays its terminal output (companion.md Part 4, one channel) → your
reply lands on its Goal and goes straight back into stdin as the next turn. Without this
the loop is one-directional and dies at the first idle.companion.md Part 4, words reach a running turn).Answers are read out of whatever shape they arrive in (spokenText) — a tapped option,
a spoken sentence, a whole call log — because the words are what the agent needs, not
the envelope. A silence is never turned into a prompt; the agent must not end up talking
to itself on your behalf.
It is not a chat client. The terminal stays where you work; this window is config plus a log of what's being mirrored. A second place to read agent output would defeat the point — the second place is your phone.
Buzz taught us the default (block/buzz auto-approves every session/request_permission
with allow_once): an agent that stalls on every tool call is an agent nobody runs.
So bypass is the default mode — the driver pins the adapter's permission mode to
bypassPermissions and answers whatever still asks — but unlike Buzz, every
auto-approval still lands on the rail as history. Bypass means "don't stall the
agent", never "don't tell the user".
ask mode is the original #804 behavior, one toggle away: blocked actions banner (or
ring, when destructive) and your answer unblocks the process. Ask pins the adapter to
default (manual prompts) explicitly — claude-agent-acp otherwise opens in auto, a
classifier that answers permission prompts in the user's place, which is precisely the
job this mode reserves for the user.
Mirrored turns and blocking asks ride the same notifications rail. What separates
silent history from "answer me now" is urgency, not a second store:
| Event | Level | Why |
|---|---|---|
| a blocked action | banner | claims attention; escalates on its own if unanswered |
| a blocked destructive action | call | rm -rf doesn't get to wait for the escalation ladder |
| a turn that failed | push | worth knowing soon, not worth a sound |
| our own parse failure | — | not on the rail at all; it's our problem, not yours |
paigy/level.ts decides; apps/api/src/arbitration/arbitrate.ts still arbitrates against your
session mode and account permissions and can only lower it.
A tool call is NOT on that table, and that is deliberate: the ⚙ line is the live texture of
a turn, and shipping it to the rail would be the working log arriving one tool at a time.
But "Working" is equally true of four minutes of pnpm test and four minutes of an agent
gently going nowhere, and the only place that told them apart was a log window on a machine
you'd walked away from.
So the tail takes a different road (paigy/activity.ts): the last two ⚙ lines ride the
presence beat, on the session's own token, and the agent's page on the phone renders
them under its standing sentence. Every property that makes it not-a-notification is load-bearing:
isLive, the same window as the dot). A tail over a dead
run is the working-over-a-corpse state the party already refused.entryFor still returns null for every work event, and
bridge.test.ts pins it — that assertion is now the guard on this feature too.Only sessions the harness drives have one. A hatched identity used straight from a terminal
(PAIGY_AGENT="…" claude) emits no work events, so its page shows no strip rather than an
empty box. Note also that presence is not in the E2EE lane (e2ee/flow.md covers
notifications and responses) — which is why the publisher clips each line and shortens
absolute paths to their last two segments before it leaves the machine.
Owner, 2026-08-28: "i feel like the desktop app also needs a space for the broker, and the queue of things to deal with after that."
Every agent on this Mac had a face on the perch and a page behind it. Paigy — the one
actually holding the work, since a note is a request addressed to the broker — had none,
even though this is the machine its sweep runs on (paigy-harness triage, #1255). You
could produce a proposal here and then had to pick up a phone to act on it.
So the home screen gains a broker card (the mark, and one standing sentence: how many open notes, when it last swept, whether something is waiting on a click), and behind it a page:
GET /api/triage's open proposal, grouped exactly as the
phone groups it: close, stale, and one card per hatchling with the queue it would
receive underneath it. One button per group, POST /api/triage/:id/accept with no
noteIds — the same endpoint the phone's button and the CLI's --apply hit, so the
three doors cannot mean three different things by "accept". Nothing on the page has
happened; accepting is the user's, always.why a sweep would quote (no movement in 40 days). Opening a note and
answering it still belongs to the phone; this is the shape of the pile, which is what
the machine can say.runTriage directly, not our own CLI: it is already a
function with injected network deps, and a shell-out would read the DEFAULT ~/.paigy
slot while this window is deliberately the Desktop one. A refusal (no judge on this
machine) stays on screen with the exact install line; a clean run's transcript goes,
because the cards it just produced say it better.src/broker.ts is the whole read, and it computes nothing of its own: the queue is
noteSignals() — literally the dossier the sweep judges — so the card and the proposal
above it can never disagree about what "open" or "40 days idle" means.
The other thing Buzz got right: connecting a harness should be a status line and a
button, not a wiki page. harness/catalog.ts is a compiled-in table per runtime — the
binaries to probe (PATH plus the dirs a GUI app can't see; Finder-launched apps
don't get your shell PATH), an auth probe (codex login status), a login hint, and an
install one-liner. The window renders it as a doctor: ✓ ready, ◐ needs login (with the
command to run), ✗ missing (with an Install button — the command comes from the
catalog, never the renderer). Starting a half-configured harness doesn't fail
silently: the hint lands on your Paigy rail as a push (nudgeSetup).
Both harnesses speak ACP — the Agent Client Protocol, JSON-RPC over stdio — through
their adapters: @agentclientprotocol/claude-agent-acp and …/codex-acp. One driver
(acp.ts), no parser museum; ACP gave Codex the permission channel codex exec never
had, and it's the door to every other harness that speaks it (Goose, Cursor, Devin…)
as a catalog entry. The stream-json Claude adapter this replaced lives in git history.
src/harness/ events.ts the neutral HarnessEvent the driver produces
acp.ts the ACP driver — both harnesses, any ACP agent tomorrow
catalog.ts which harnesses exist, detection, auth probes, installs
session.ts spawn, line-split, lifecycle — the only impure file here
src/paigy/ level.ts event → NotifyLevel
bridge.ts permission asks, cannot-work notices, the turn's end as Goal progress
conversation.ts the round trip — your words into a running agent's stdin
src/triage/ evidence.ts the deterministic signals; judge.ts the one model boundary
verdict.ts the prompt + its defensive parser; triage.ts the sweep itself
src/broker.ts what Paigy is HOLDING — the open queue + the one open proposal, one read
src/run.ts the whole bridge, Electron-free — what main.ts and cli.ts both drive
src/main.ts Electron main; renderer/ is a doctor, a form and a log
src/cli.ts `paigy-harness` — the same bridge from a terminal or a service unit
Parsing lives in the drivers so the whole protocol surface is testable without spawning anything.
Unpaired (or still borrowing a legacy ~/.paigy slot), the window shows a QR — the
device flow's verification URL. Scan it with the phone camera, approve on the phone,
and the token lands in the app's own Desktop slot: the machine becomes its own
identity ("Mauricio's Mac"), sessions and hatched agents mint underneath it, and the
default slot goes back to belonging to whatever terminal agent paired it. Legacy
setups keep working until scanned — the beacon is an upgrade, never a wall.
An allow-list of folders at ~/.paigy/workspaces.json — device config, shared by the
window and the headless host, so the phone's offer never depends on which entry point
is running. A session may only start inside a granted folder (the desktop equivalent
of a permission scope), curated through the window's native picker or
paigy-harness host --grant DIR. This list is exactly what the phone offers when you
launch or assign a session. The CLI's positional-run path deliberately does NOT
enforce it: a path typed into your own shell is its own grant.
While a host is running (window open, or paigy-harness host), its heartbeat
advertises ready harnesses + granted workspaces. On the phone: Agents → "+ New
session" picks from those options and gives the session the pairing flow's identity
gestures — a name and a voice — because a session IS a minted pairing: its turns and
everything you say back ride its own conversation. The notes assign picker offers
the same as "New session · " rows, naming the session after the
note and delivering the brief as its opening request.
paigy-harness hatch "Name" mints a pre-paired sibling identity under your account —
your existing pairing is the ceremony; no code, no phone round-trip. It lands in its
own ~/.paigy slot:
paigy-harness hatch "Voice bug hunter"
PAIGY_AGENT="Voice bug hunter" claude # any tool speaks as it
paigy-harness --identity "Voice bug hunter" --harness codex "fix the flaky test"
Hatched agents appear on the phone's Agents screen like any pairing — renameable, re-voiceable, revocable.
curl -fsSL https://paigy.ai/install | sh
Installs the npm CLI (one self-contained file — the workspace libs are bundled) and
runs paigy-harness setup: QR-pairs the machine, hatches each detected agent's
identity (no codes), registers the Paigy MCP in every harness it runs (MCP_WIRES —
Claude Code, Codex, Antigravity: an agent's only channel to you) + the statusline,
grants a workspace, installs
the host service. paigy-harness service installs a
macOS launchd agent so hosting survives reboots — the host loop IS the service,
launchd just keeps it alive (the old "no daemon" note was about the WAKE channel,
which still belongs to paigy-listen).
npm is the ONLY shipping channel: @paigy/harness installs the CLI and the whole
bridge. The host service updates itself once no session is running (src/update.ts), and
re-running curl -fsSL https://paigy.ai/install | sh updates it on demand and restarts it —
a bare npm i -g leaves the running host on the old code. The Electron window runs
from source (electron .) — there is no packaging, no Developer ID signing or
notarization, and therefore no auto-update (macOS auto-update requires the signing).
A downloaded unsigned app would hit Gatekeeper's "damaged" dialog, which is worse
than no download button. Steps for when it's time: #854.
pnpm --filter @paigy/harness dev # the window (status + host; sessions start from the phone)
pnpm --filter @paigy/harness build
node apps/desktop/dist/cli.js --doctor # or headless: paigy-harness
node apps/desktop/dist/cli.js --harness codex --cwd ~/repo "fix the flaky test"
node apps/desktop/dist/cli.js host --grant ~/projects # standby, launchable from the phone
The window deliberately has NO run form — launching moved to the phone (companion.md). It shows pairing, the party, the broker's card and queue, the doctor, the workspace allow-list, a Stop button, and the log.
Headless flags: --harness claude|codex, --mode bypass|ask, --cwd DIR,
--identity NAME, --grace SECONDS, --doctor; subcommands host [--grant DIR]…
and hatch NAME. In ask mode the CLI is TERMINAL-FIRST: a blocked action or open
question prints in the terminal and waits --grace (default 90s); a person at the
keyboard answers in place, an unattended terminal escalates to your phone (0 = phone
immediately; non-TTY runs always go straight to the phone).
The CLI is the proof the harness doesn't need the app: run.ts/host.ts never
import Electron — a machine with only the CLI is exactly as launchable as one with
the window.
Pairing is one-time and lives in ~/.paigy — the app reads it, never mints it. If
you've never paired: paigy-harness setup.
curl -fsSL https://paigy.ai/uninstall.sh | sh removes the harness surface (service,
global package, workspace list, the machine's Desktop slot — other agents' slots
survive). Then revoke the machine on the phone's Agents screen: server-side tokens
outlive local files.
The FULL Paigy strip (tested 2026-08-03) additionally removes, in order: every
~/.paigy slot (unpairs ALL local agents — re-pairing resets voices), the ACP
adapters (npm rm -g @agentclientprotocol/{claude-agent-acp,codex-acp}), Electron
userData, the statusline block + paigy@paigy plugin enablement + marketplace +
mcp__paigy__* permission entries in ~/.claude/settings.json, the project
.mcp.json paigy server, the plugin cache/registry entries, and ~/.npm/_npx (so
@paigy/mcp@latest can't serve stale). Hooks can recreate ~/.paigy — delete it
LAST, after the hooks are gone.
stream-json nor ACP adapter
behavior is a stability contract. Every driver fails soft by design: an unrecognized
frame is skipped, never fatal. If mirroring goes quiet after a CLI update, re-verify
the frame names in the driver doc comments first. An adapter can also leave a prompt
unanswered for good (claude-agent-acp#1145); the driver cancels one after a long quiet
with no tool running (companion.md Part 4, one way in).claude auth status, codex login status) and
can drift with CLI releases; a probe failure reads as "needs login", so drift shows
up as a nagging hint, not a broken start.run.ts binds
submit/check/ack once per session), so parallel sessions never share a
conversation. "Stop sessions" stops them all; per-session stop is UI away.paigy-harness service, launchd, KeepAlive) —
it claims phone launches and runs sessions. The WAKE channel for terminal-agent
replies still belongs to paigy-listen + PAIGY_ON_WAKE (#486); spawn-on-wake
needs the slot-lease design in companion.md before it auto-wires.FAQs
Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.
The npm package @paigy/harness receives a total of 771 weekly downloads. As such, @paigy/harness popularity was classified as not popular.
We found that @paigy/harness demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.