Skip to content

feat(hook): o host roteia todo comando por codemode, e avisa quando a rajada devia ter sido script - #102

Merged
SouzaRodrigo61 merged 5 commits into
mainfrom
feat/hook-pretooluse
Aug 30, 2026
Merged

SouzaRodrigo61 merged 5 commits into
mainfrom
feat/hook-pretooluse

Conversation

@SouzaRodrigo61

Copy link
Copy Markdown
Owner

Por que

codemode exec ja se anunciava como "the path a host's shell tool should call", mas
faltava o verbo que fala o protocolo do host. Quem queria isso instalava rtk hook claude
-- um segundo binario no PreToolUse, embrulhando por fora o mesmo rtk que ja roda dentro
deste desde a 0.2.0.

E faltava o outro lado: o codemode run colapsa N chamadas em uma, mas so quando o modelo
escolhe escrever o script -- e ele descobre a terceira chamada uma de cada vez, cada uma
parecendo "so mais uma verificacao". Medido numa sessao real: 76 Bash, zero script.

O que entra

codemode hook claude -- le o JSON da chamada na stdin e devolve o comando reescrito
como codemode exec -- '<cmd>', em um argumento (argv-de-um e linha de shell no
exec_one, e e isso que preserva pipe, && e redirect). Num spawn so: filtro do rtk,
denylist, corte de saida e telemetria -- que ate agora so via script, nunca comando solto.

--wrap <prefixo> -- um host aceita UMA reescrita por chamada. Dois hooks que respondem
updatedInput nao se compoem: o ultimo apaga o outro, e qual e o ultimo e sorteio (medido:
metade do trafego ia por um roteador, metade pelo outro). --wrap encadeia dentro de uma
resposta so: <wrap> codemode exec -- '<cmd>'. O embrulhado fica por fora e recebe a saida
ja filtrada; o codemode continua vendo o comando original, entao o roteamento e o verbo da
telemetria seguem certos. O codemode nao sabe o que e o programa embrulhado, de proposito.

--post -- conta a rajada por sessao (estado em ~/.codemode/bursts/, verbo e
workdir, nunca a linha
) e injeta uma linha de contexto ao passar de 5 comandos no mesmo
diretorio em 180s. Teto de 3 por sessao, rajada zerada a cada aviso, silencio total quando
o modelo roda codemode run. E pergunta, nao acusacao: o hook nao sabe se a serializacao
era legitima.

Duas correcoes, uma delas de coisa quebrada

  • run_in_background passa cru. O exec so devolve a saida no fim e mataria em
    --cmd-timeout: servidor de dev e tail -f nunca responderiam.
  • Builtin de estado sozinho passa cru (cd, export, source, set, trap...).
    Rodar num filho e no-op silencioso -- o host le pwd no shell pai depois do comando.
    cd /x && cargo test continua reescrito, porque ali o cd vale pro comando que vem junto.
  • Telemetria grava o verbo, nao a linha. Com a linha inteira chegando num argumento so,
    cmd.first() passou a gravar caminho, header e token no runs.jsonl.

Como testar

cargo test --bin codemode hook::          # 16 testes do modulo
echo '{"tool_name":"Bash","tool_input":{"command":"git status"},"cwd":"/tmp"}' | codemode hook claude

No settings.json do Claude Code:

{ "hooks": {
  "PreToolUse":  [{ "matcher": "Bash", "hooks": [{ "type": "command",
     "command": "'/abs/path/codemode' hook claude" }] }],
  "PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command",
     "command": "'/abs/path/codemode' hook claude --post" }] }]
} }

Caminho absoluto importa: sessao spawnada com PATH minimo perde o hook calada.

Evidencia viva

Numa sessao real, o aviso saiu sozinho na quinta chamada, com os verbos extraidos atraves
do embrulho:

codemode: 5 comandos em 28s no mesmo diretorio (ls x2, echo, python3, for). Se os proximos dois ja estao decididos, isso e um script e nao N chamadas -- codemode list antes.

Custo medido: hook 0-10ms, contra ~822ms de round-trip por tool-call.

O que fica torto

  • Linha que comeca com palavra-chave de shell (for i in ...; do) reporta for como verbo.
    Cosmetico; consertar exige interpretar keyword de shell.
  • 5/180s/3 sao desenho, nao medida. Em uma semana o codemode gain diz se a razao
    exec vs run mexeu. Se nao mexer, o hook e ruido e sai.

Co-authored com Claude Code.

🤖 Generated with Claude Code

SouzaRodrigo61 and others added 5 commits August 30, 2026 10:18
O `codemode exec` já se anunciava como "the path a host's shell tool
should call", mas faltava o verbo que fala o protocolo do host: quem
queria isso tinha que instalar `rtk hook claude` — um segundo binário
no PreToolUse, embrulhando por fora o mesmo rtk que já roda dentro
deste desde 0.2.0.

`codemode hook claude` lê o JSON da chamada na stdin e devolve o
comando reescrito como `codemode exec -- '<cmd>'`, em UM argumento
(argv-de-um é linha de shell no exec_one, e é isso que preserva pipe,
`&&` e redirect). Com isso o host ganha, num spawn só: filtro do rtk
(in-process onde já foi migrado), denylist, corte de saída e a
telemetria que alimenta `codemode gain` — que até agora só via script,
nunca comando solto.

Dois casos não respondem nada de propósito, e o host segue com o
comando original e a permissão que ele já tomaria: o que a denylist
pega (reescrever `rm -rf` seria pedir allow pro que existe pra ser
perguntado) e o que já começa com `codemode `/`rtk ` (idempotência).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dois hooks que respondem `updatedInput` na mesma chamada não se compõem:
o último a responder apaga o outro, e qual é o último é sorteio. Medido
na máquina: metade dos comandos ia por um roteador, metade pelo outro.

`--wrap <prefixo>` resolve encadeando dentro de UMA resposta:
`<wrap> codemode exec -- '<cmd>'`. O programa embrulhado fica por fora e
recebe a saída já filtrada; o codemode continua vendo o comando
original, então o roteamento do rtk e o verbo da telemetria seguem
certos -- embrulhar ao contrário faria todo comando ter o verbo do
embrulhador.

O codemode não sabe o que é o programa do --wrap, e é de propósito: a
mesma guarda de idempotência do bloco acima vale pra ele, comparando
basename sem aspas (o prefixo chega com caminho absoluto, o comando que
o agente escreve à mão vem curto).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cmd.first()` era o primeiro ARGUMENTO. Desde que o hook passou a mandar
a linha inteira num argumento só (é o que preserva pipe e `&&`), isso
virou a linha completa dentro de `runs.jsonl` -- caminho, header, token,
tudo que passar num comando. O próprio comentário logo abaixo promete
"metadado, nunca conteúdo".

Passa a ser a primeira palavra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o deve

O PreToolUse não colapsa nada: quando ele roda, a chamada já foi
decidida. Quem colapsa é o modelo escolhendo escrever um script, e ele
descobre a terceira chamada uma de cada vez -- cada uma parecendo "só
mais uma verificação". Medido numa sessão real: 76 Bash, zero script.

`--post` conta a rajada por sessão (estado em ~/.codemode/bursts/,
verbo e workdir, nunca a linha) e injeta UMA linha de contexto quando
passa de 5 comandos no mesmo diretório dentro de 180s. Teto de 3 por
sessão, rajada zerada a cada aviso, e silêncio total quando o modelo
roda `codemode run` -- quem obedeceu não leva sermão. É pergunta, não
acusação: o hook não sabe se a serialização era legítima.

E duas correções no caminho de ontem, das quais uma estava quebrando
coisa em produção:

- `run_in_background` passa cru. O `exec` só devolve a saída no fim e
  mataria em --cmd-timeout: servidor de dev e `tail -f` nunca
  responderiam.
- builtin de estado sozinho (`cd`, `export`, `source`...) passa cru.
  Rodar num filho é no-op silencioso, e o host lê `pwd` no shell pai
  depois do comando. `cd /x && cargo test` continua reescrito, porque
  aí o `cd` vale para o comando que vem junto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A rajada reportou `cd×5` para cinco chamadas que rodavam coisas
diferentes, e `for` para um laço -- rótulo que esconde justamente o que
se queria ver. O extrator pegava o primeiro token da linha, e com o hook
mandando tudo num argumento só isso é `cd` em `cd /x && cargo test`.

Agora há duas classes. Cabeça que invalida o SEGMENTO inteiro: builtin
de estado (o comando é o que vem depois do `&&`) e abertura de laço ou
condicional (`for i in 1 2 3` -- `i` é a variável, não um programa).
E cabeça transparente, onde o comando é o próximo token: `do`, `then`,
`sudo`, `env`, `timeout`, `nice` e afins, mais atribuição (`FOO=1 cmd`)
e número (`timeout 600 cargo`).

Mesmo extrator agora serve a telemetria do `exec`, que tinha o mesmo
defeito pelo mesmo motivo -- era `cd` no `runs.jsonl` de toda linha
composta.

Sem consciência de aspas de propósito: `;` dentro de string parte o
segmento no lugar errado. É rótulo, não parser, e erra para o lado de
mostrar o comando seguinte em vez de esconder tudo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SouzaRodrigo61
SouzaRodrigo61 merged commit 7e2a6f7 into main Aug 30, 2026
1 of 2 checks passed
@SouzaRodrigo61
SouzaRodrigo61 deleted the feat/hook-pretooluse branch August 30, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant