feat(hook): o host roteia todo comando por codemode, e avisa quando a rajada devia ter sido script - #102
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Por que
codemode execja se anunciava como "the path a host's shell tool should call", masfaltava 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 dentrodeste desde a 0.2.0.
E faltava o outro lado: o
codemode runcolapsa N chamadas em uma, mas so quando o modeloescolhe 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 reescritocomo
codemode exec -- '<cmd>', em um argumento (argv-de-um e linha de shell noexec_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 respondemupdatedInputnao 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).
--wrapencadeia dentro de umaresposta so:
<wrap> codemode exec -- '<cmd>'. O embrulhado fica por fora e recebe a saidaja 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 eworkdir, 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 serializacaoera legitima.
Duas correcoes, uma delas de coisa quebrada
run_in_backgroundpassa cru. Oexecso devolve a saida no fim e mataria em--cmd-timeout: servidor de dev etail -fnunca responderiam.cd,export,source,set,trap...).Rodar num filho e no-op silencioso -- o host le
pwdno shell pai depois do comando.cd /x && cargo testcontinua reescrito, porque ali ocdvale pro comando que vem junto.cmd.first()passou a gravar caminho, header e token noruns.jsonl.Como testar
No
settings.jsondo 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:
Custo medido: hook 0-10ms, contra ~822ms de round-trip por tool-call.
O que fica torto
for i in ...; do) reportaforcomo verbo.Cosmetico; consertar exige interpretar keyword de shell.
codemode gaindiz se a razaoexecvsrunmexeu. Se nao mexer, o hook e ruido e sai.Co-authored com Claude Code.
🤖 Generated with Claude Code