Status: Proposed, 2026-10-12. Amends 0004, within 0003.
Context#
Vults answers a permission only for the agents it knows: each one reads its own reply shape (Claude
Code’s hookSpecificOutput, Copilot CLI’s bare behavior), so each was added by hand after a
recorded session. Any other tool can already put its sessions on the wire with
vults-hook --agent <name> and Claude Code’s hook JSON, but its PermissionRequest is shown as a
question waiting in its terminal and the hook never waits: nothing in the app knows what that tool
would read back (see Permissions in Other agents).
That keeps the door shut for the tools people build themselves (scripts, in-house agents, orchestrators running several agents), which are what the flock and its cards are for. A tool that wants a card should be able to ask for one, if it takes one fixed answer and honors it.
Decision#
- A tool asks for the reply. Its hook command adds
--reply vults:vults-hook --agent <name> --reply vults PermissionRequest. Without it nothing changes: the hook never waits for a named tool, as today. Built-in names (claude,codex,copilot,gemini,opencode,qwen,other, and every agent the registry lists, such ascursororkimi) cannot take it. - It sends Claude Code’s hook JSON, the format most tools already copy: a
PermissionRequestwithsession_id,cwd,tool_nameandtool_input. The card is built from it as for any agent: the whole command, file or URL, never a summary the tool wrote. - It reads one fixed reply on stdout, one JSON line:
{"decision":"allow"}or{"decision":"deny","reason":"Denied from Vults"}. Anything else, empty stdout included, means ask the user yourself: no click in time, the app closed or paused, an unsupported protocol version, any failure. The hook still exits 0 and never blocks the tool (0003); silence is never an allow. - Who answers is who answers today (0004): a click, a signed answer from a paired phone (0004), or an exact Always rule the user made on that tool’s own card. Rules are kept by the tool’s name, its folder, the tool and the target: a rule made for one named tool never answers another, nor a built-in agent.
- Policies (0004) reach a named tool only when they name it
(
agents my-tool), and then only to deny or ask: a policy never allows a named tool’s call, as it never allows any agent’s but Claude Code’s today. A destructive call always asks (0004). Every answer is audited with the tool’s name. - A named tool gains nothing a built-in agent’s card would not show. It reaches the same
socket any process of the user’s can already reach (same user, checked by
peer); its card shows exactly what it sent; an Allow goes back only to the call that asked. Vults cannot make a tool honor the reply, as it cannot make any agent: a tool that ignores it is one that never asked. - A name that becomes a built-in agent later loses its rules: they are not carried over to an agent that reads another reply.
Consequences#
- G2 builds it: the hook’s flag and reply, a protocol
VERSIONbump (an old app answersunsupported, so the tool asks itself), rules and policies keyed by name in the core, the named tools in Settings → Agents with their rules, and Hook protocol written as a spec with an example tool in shell and in TypeScript. CLAUDE.md’s rule 2, Safety and Other agents say a tool that asks with--reply vultsgets a card, and how it is answered.- A test tool in shell gets a card, an allow and a deny, and asks itself when the app is closed; an old hook stays silent.