Claude Code, Codex, Antigravity, OpenCode, Qwen Code, Copilot CLI, Cursor, Factory Droid e Kimi Code já vêm integrados (Settings → Agents). Qualquer outra ferramenta também pode colocar suas sessões no fio, se ela rodar um comando nos seus eventos e mandar o JSON de hook do Claude Code pelo stdin (o formato que a maioria das ferramentas de agente copia). Para ferramentas que rodam plugins em vez disso (Pi), um plugin curto faz o mesmo; as receitas estão mais abaixo.
Aponte os hooks da ferramenta para o relay, com um nome à sua escolha (no Windows o relay fica em
%LOCALAPPDATA%\Vults\bin\):
~/.local/share/vults/bin/vults-hook --agent my-tool SessionStart- O nome tem de 1 a 24 caracteres de
a-z,0-9e-, e não pode ser um para o qual o fio tem um tipo (claude,codex,copilot,opencode,qweneother), nemgemini: o fio não tem mais um tipo para ele, mas os hooks antigos do Gemini CLI podem continuar instalados, então o nome segue reservado e eles não mandam nada. Os integrados que o Vults manda por nome (antigravity,cursor,droid,kimi) usam esses nomes eles mesmos, então não ficam reservados; uma ferramenta com qualquer nome não ganha card, inclusive um integrado mandado por nome. Com um nome que quebra essas regras, o hook não manda nada e sai na hora. - O nome do evento vem de
hook_event_nameno JSON, ou do último argumento. Os nomes de evento do Claude Code são lidos, e os do Cursor também. - Cada sessão precisa de um id:
session_id, ouconversation_id,sessionIdouconversationId. Um evento sem id é descartado. As sessões aparecem com o nome da ferramenta, em violeta.
Os campos que ela lê estão em Protocolo do hook. Para essas
ferramentas, o relay sempre sai com 0 e não imprime nada, mas um relay ausente faz o comando falhar:
termine todo comando de hook em || exit 0, ou uma ferramenta que trata um hook com falha como um
“não” para de funcionar quando o app não está instalado.
O que cada agente consegue fazer#
Settings → Agents, em What each agent can do, mostra cada agente embutido: se a CLI dele está no PATH e a versão, se os
hooks estão instalados e atualizados (e, no Codex, confiáveis), e o que ele consegue fazer com o
Vults. Check again pergunta de novo; a resposta fica guardada por dez minutos.
| Permissões | Perguntas | Diffs | Parar um turno | Sandbox | Iniciado pelo Vults | |
|---|---|---|---|---|---|---|
| Claude Code | um card | um card | linhas, numeradas | na hora, quando o Vults o iniciou | bubblewrap, se ligado | sim |
| Codex | um card | o terminal dele | linhas | na hora, quando o Vults o iniciou | o modo de sandbox dele | sim |
| Antigravity | o terminal dele | o terminal dele | nenhum | pelo Vults, ainda não | nenhum | ainda não |
| OpenCode | um card | um card (OpenCode 1) | linhas | na hora, quando o Vults o iniciou | nenhum | uma tarefa, por ACP |
| Qwen Code | um card | o terminal dele | linhas | na hora, quando o Vults o iniciou | um contêiner, se ligado | uma tarefa, por ACP |
| Copilot CLI | um card | nenhuma | linhas | na hora, quando o Vults o iniciou | nenhum | uma tarefa, por ACP |
| Cursor | o terminal dele | o terminal dele | nenhum | não pelo Vults | nenhum | não |
| Factory Droid | o terminal dele | o terminal dele | linhas | não pelo Vults | nenhum | não |
| Kimi Code | o terminal dele; um card numa tarefa | o terminal dele | linhas | na hora, quando o Vults o iniciou | nenhum | uma tarefa, por ACP |
O Vults para um turno, e mantém a sessão, só numa sessão que ele iniciou (a do chat, ou a de uma
tarefa): nenhum sinal faz isso, um sinal encerra a sessão inteira. OpenCode, Qwen Code e
Copilot CLI pegam tarefas pelo Agent Client Protocol (Start with
another agent… na aba Tasks, quando o agente está instalado); veja
Tarefas. O sandbox do Codex depende do modo:
workspace-write escreve só na pasta dele, nas pastas temporárias e nas pastas que recebe. Se
ligado: o sandbox fica desligado até você ligar nesse agente. Nenhum desses sandboxes foi testado
com o Vults ainda.
Permissões#
A ilha nunca responde à permissão de uma ferramenta que não vem integrada: ela não tem como saber o
que essa ferramenta espera de volta. Um PermissionRequest aparece como uma pergunta esperando no
terminal, o hook não espera, e a ferramenta pergunta a você lá, como de costume.
Antigravity#
Settings → Agents → Antigravity instala um hook chamado vults em ~/.gemini/config/hooks.json,
o arquivo que o CLI agy, o app Antigravity e a IDE compartilham (testado com o agy 1.2.16). Os
outros hooks dali nunca são tocados; se um hook seu já se chamar vults, o instalador não escreve
nada. As sessões dele aparecem como antigravity.
A ilha mostra o Antigravity pensando, cada passo (view_file, run_command…) e quando ele para. O
Antigravity não manda a resposta dele, então uma sessão terminada não tem linha de resumo, e não há
início nem fim de sessão: uma sessão sai do fio depois de um tempo em silêncio (10 minutos depois de
terminar, 30 nos outros casos). O hook dele roda antes de toda ferramenta e não diz se o Antigravity
teria perguntado, então o Antigravity pede as permissões ele mesmo
(Aprovando).
Cursor#
Settings → Agents → Cursor adiciona os hooks dele a ~/.cursor/hooks.json, o arquivo que o
editor e o cursor-agent compartilham; os hooks de outras ferramentas nele ficam, e Remove
hooks… tira só os do Vults. Reinicie o Cursor depois. As sessões dele aparecem como cursor, no
projeto em que o Cursor trabalha.
A ilha mostra cada passo terminado, falhas incluídas, e quando um turno para. Os hooks dele que
rodam antes de um passo (preToolUse, beforeSubmitPrompt, beforeShellExecution…) ficam de fora:
o Cursor lê o que eles imprimem como uma decisão, então o Cursor pergunta as permissões ele mesmo
(Aprovando). O que uma ferramenta imprimiu e o email da conta nunca saem do
hook.
O Cursor também roda os hooks do Claude Code (os hooks third-party dele, ligados por padrão). O relay ignora os eventos do Cursor ali, então uma sessão nunca aparece duas vezes, uma como Claude Code.
Factory Droid#
Settings → Agents → Factory Droid adiciona os hooks dele a ~/.factory/settings.json (o
Droid lê os hooks do Claude Code; o resto do arquivo e os hooks de outras ferramentas ficam).
Reinicie o Droid depois. As sessões dele aparecem como droid (conferido com o droid 0.239.0).
A ilha mostra cada passo quando começa e termina, as linhas de uma edição e quando um turno para.
Um comando que falha não manda evento próprio: o passo dele termina com o turno. O Droid não tem
evento de permissão, e o PreToolUse dele roda antes de toda ferramenta, pedisse ele ou não, então
o Droid pergunta as permissões ele mesmo (Aprovando).
Kimi Code#
Settings → Agents → Kimi Code adiciona os hooks dele a ~/.kimi-code/config.toml
(KIMI_CODE_HOME o move), como tabelas [[hooks]] ao lado das suas configurações: seus comentários
e a formatação ficam, e os hooks de outras ferramentas também. Reinicie o Kimi depois. As sessões
dele aparecem como kimi (conferido com o Kimi Code 2.1.1).
A ilha mostra cada passo, as linhas de uma edição, um comando que falhou e quando um turno para. O
Kimi avisa o hook PermissionRequest sobre uma permissão sem esperar resposta, então a ilha mostra
que ela espera no terminal do Kimi e o Kimi pergunta ele mesmo (Aprovando).
O Kimi também pega tarefas por ACP (kimi acp), e ali cada escrita e cada comando é um card da ilha.
A sessão dele é posta no modo do Kimi que pergunta quando começa: o seu default_permission_mode na
config dele, senão, rodaria toda ferramenta sem perguntar, enquanto a sessão ainda diz que pergunta.
OpenCode#
Settings → Agents → OpenCode escreve um plugin em ~/.config/opencode/plugins/vults.js, com o
mesmo diff, backup e clique dos hooks; reinicie o OpenCode depois. Ele informa cada passo e coloca as
permissões e as perguntas do OpenCode na ilha (Aprovando). Um arquivo ali
sem a primeira linha Generated by Vults é seu, e o instalador não mexe nele. O OpenCode não tem um
evento para quando ele fecha, então os pássaros dele saem do fio depois de um tempo em silêncio.
Receitas#
Estas receitas foram conferidas com a documentação e a cópia instalada de cada ferramenta (versões abaixo), inclusive com o relay ausente; nenhuma foi rodada dentro de uma sessão real da ferramenta. Cada uma só informa: nunca responde a uma permissão.
Pi#
O Pi (1.0) carrega extensões de ~/.pi/agent/extensions/. Salve isto como
~/.pi/agent/extensions/vults.ts. Ele escuta tool_execution_start e _end, nunca tool_call:
um handler de tool_call que falha para a ferramenta.
import { spawn } from "node:child_process";
import { homedir } from "node:os";
import { join } from "node:path";
const RELAY = join(homedir(), ".local/share/vults/bin/vults-hook");
function toolInput(args: any): Record<string, unknown> {
const a = args ?? {};
const out: Record<string, unknown> = {};
if (typeof a.command === "string") out.command = a.command;
if (typeof a.path === "string") out[a.pattern === undefined ? "file_path" : "path"] = a.path;
if (typeof a.pattern === "string") out.pattern = a.pattern;
return out;
}
let queue: Promise<void> = Promise.resolve();
function run(event: string, payload: string): Promise<void> {
return new Promise((done) => {
try {
const child = spawn(RELAY, ["--agent", "pi", event], { stdio: ["pipe", "ignore", "ignore"], detached: true });
setTimeout(done, 2000).unref();
child.on("error", () => done());
child.on("exit", () => done());
child.stdin?.on("error", () => {});
child.stdin?.end(payload);
child.unref();
} catch { done(); }
});
}
function send(event: string, ctx: any, extra: Record<string, unknown> = {}): void {
try {
const payload = JSON.stringify({ hook_event_name: event, session_id: ctx.sessionManager.getSessionId(), cwd: ctx.cwd, ...extra });
queue = queue.then(() => run(event, payload));
} catch {}
}
function text(content: any): string | undefined {
if (!Array.isArray(content)) return undefined;
const s = content.filter((c) => c?.type === "text").map((c) => c.text).join("\n").trim();
return s || undefined;
}
export default function (pi: any) {
let last: string | undefined;
let failed = false;
pi.on("session_start", (_e: any, ctx: any) => send("SessionStart", ctx));
pi.on("session_shutdown", (_e: any, ctx: any) => send("SessionEnd", ctx));
pi.on("before_agent_start", (_e: any, ctx: any) => { last = undefined; failed = false; send("UserPromptSubmit", ctx); });
pi.on("tool_execution_start", (e: any, ctx: any) =>
send("PreToolUse", ctx, { tool_name: e.toolName, tool_input: toolInput(e.args) }));
pi.on("tool_execution_end", (e: any, ctx: any) =>
send(e.isError ? "PostToolUseFailure" : "PostToolUse", ctx, { tool_name: e.toolName }));
pi.on("agent_end", (e: any, ctx: any) => {
try {
const reply = [...(e.messages ?? [])].reverse().find((m: any) => m?.role === "assistant");
if (reply?.stopReason === "error") { failed = true; send("StopFailure", ctx, { error: reply.errorMessage }); }
else last = text(reply?.content) ?? last;
} catch {}
});
pi.on("agent_settled", (_e: any, ctx: any) => { if (!failed) send("Stop", ctx, { last_assistant_message: last }); });
}ctx.sessionManager.getSessionId() existe no Pi, mas não está na documentação dele; se um Pi futuro
o remover, a extensão não manda nada em vez de falhar.
Testando#
echo '{"hook_event_name":"SessionStart","session_id":"s1","cwd":"'$PWD'"}' \
| ~/.local/share/vults/bin/vults-hook --agent my-tool
echo '{"hook_event_name":"PreToolUse","session_id":"s1","tool_name":"Bash","tool_input":{"command":"make"}}' \
| ~/.local/share/vults/bin/vults-hook --agent my-tool
echo '{"hook_event_name":"SessionEnd","session_id":"s1"}' \
| ~/.local/share/vults/bin/vults-hook --agent my-toolO protocolo por trás disso está em Protocolo do hook.