Guardrail para apps integrados ao Adaflow: impede vazar o app token (x-ada-token) para o browser, para o codigo-fonte ou para o transcript

Mod do Claude Code que impede vazar o app token (x-ada-token) do Adaflow para o browser, para o código-fonte ou para o transcript. Funciona como guardrail: bloqueia ou avisa nas ferramentas Write, Edit e Bash.
No terminal, em uma sessão do Claude Code:
/plugin install adaflow-guard --marketplace Adalink-ai/ada-mods
Responda y em Add marketplace? (se for a primeira vez), escolha o escopo e deixe as opções padrão.
A cada tentativa de Write, Edit ou Bash que violasse as regras, o mod intervém:
| Regra | Bloqueia quando | Ferramenta |
|---|---|---|
token-in-client | um arquivo "use client" referencia a credencial (ADAFLOW_APP_TOKEN, x-ada-token, etc.) | Write, Edit |
public-token-env | o token aparece com prefixo público (NEXT_PUBLIC_, VITE_, REACT_APP_) | Write, Edit |
browser-gateway | um client component chama o gateway direto, sem proxy | Write, Edit |
literal-token | há um token hardcoded no código (placeholders e .env real ficam de fora) | Write, Edit |
token-print | um comando imprimiria o token no transcript (echo $ADAFLOW_APP_TOKEN, printenv, etc.) | Bash |
Em modo deny (padrão) a ferramenta é recusada e o motivo, com a correção sugerida, volta para o modelo, que normalmente corrige sozinho. Em modo warn a ferramenta roda e um toast avisa. Só conta o que a edição introduz: um arquivo que já tinha o problema não trava edições que não o agravam.
| Campo | Padrão | Efeito |
|---|---|---|
mode | deny | deny bloqueia; warn deixa passar e avisa |
Mude para warn no menu do Claude Code se quiser auditar sem bloquear.
As regras seguem o guia de integração do Adaflow:
/api/adaflow/[...path]).echo, printenv) vaza para qualquer leitor da sessão.Write e Edit são inspecionados; um echo … > arquivo ou sed -i contorna as regras de arquivo."use client" (Next.js App Router). Em SPAs (Vite, Create React App) todo arquivo roda no browser, mas só a regra de prefixo público (VITE_, REACT_APP_) pega.literal-token dispara com um valor literal de 16+ caracteres em x-ada-token, appToken ou atribuído a ADAFLOW_APP_TOKEN/ADALINK_APP_TOKEN/ADA_TOKEN (placeholders como your-…/example são ignorados). Fixtures de teste podem disparar; o modo warn serve para isso.claude plugin validate .
claude plugin test .
claude --plugin-dir .hooks/register.ts 124 lines1import type { Register } from 'claude-code'
2
3// O segredo e o app token do Adaflow (header x-ada-token). O JWT do usuario e curto e vive no
4// browser por desenho; o app token e de longa duracao e so pode existir no servidor.
5const TOKEN_ENV = /\b(?:ADAFLOW_APP_TOKEN|ADALINK_APP_TOKEN|ADA_TOKEN)\b/
6const TOKEN_NAME = new RegExp(`${TOKEN_ENV.source}|x-ada-token|\\bappToken\\b`, 'i')
7
8// Variavel que o bundler entrega ao browser (NEXT_PUBLIC_, VITE_, REACT_APP_, PUBLIC_) com o token no nome.
9const PUBLIC_TOKEN = /(?<![A-Z0-9_])(?:NEXT_PUBLIC|VITE|REACT_APP|PUBLIC)_[A-Z0-9_]*(?:APP_?TOKEN|ADA_?TOKEN)/
10
11// Primeira instrucao do arquivo e a diretiva 'use client' (comentarios iniciais sao ignorados).
12const IS_CLIENT = /^\s*(?:\/\/[^\n]*\n\s*|\/\*[\s\S]*?\*\/\s*)*['"]use client['"]/
13
14// O browser fala com o app (proxy /api/adaflow/*), nunca direto com o gateway.
15const BROWSER_GATEWAY = /adalink-api-gateway\.onrender\.com|NEXT_PUBLIC_ADAFLOW_(?:BASE_URL|API_URL|GATEWAY)/
16
17const LITERALS = [
18 /x-ada-token['"`]?\s*[:=,]\s*['"`]([^'"`\s$]{16,})['"`]/gi,
19 /\bappToken\s*[:=]\s*['"`]([^'"`\s$]{16,})['"`]/g,
20 /\b(?:ADAFLOW_APP_TOKEN|ADALINK_APP_TOKEN|ADA_TOKEN)\s*[:=]\s*['"`]?([A-Za-z0-9._~+/=-]{20,})['"`]?/g,
21]
22const PLACEHOLDER = /your|seu[-_ ]|exemplo|example|placeholder|changeme|replace|todo|xxx|\*\*\*|<|>/i
23
24const hasLiteralToken = (text: string) =>
25 LITERALS.some(re => [...text.matchAll(re)].some(m => !PLACEHOLDER.test(m[1] ?? '')))
26
27const RULES: Record<string, string> = {
28 'public-token-env':
29 'o app token nao pode ter prefixo NEXT_PUBLIC_/VITE_/REACT_APP_/PUBLIC_: o bundler o entregaria ao browser. Guarde-o em variavel so de servidor (ADAFLOW_APP_TOKEN) e use-o em route handler.',
30 'token-in-client':
31 'este arquivo e client component ("use client") e referencia o app token / x-ada-token. O app token e segredo de servidor: no browser use o JWT do usuario (SSO) e passe pelo proxy /api/adaflow/[...path], que anexa a credencial no servidor.',
32 'browser-gateway':
33 'este arquivo e client component ("use client") e chama o gateway da plataforma direto. O browser nunca fala com o gateway: chame o proxy do app (/api/adaflow/[...path]) com allowlist.',
34 'literal-token':
35 'ha um token literal no codigo-fonte. Leia de process.env.ADAFLOW_APP_TOKEN (server-side) e mantenha o valor em .env/.env.local fora do git.',
36}
37
38const violations = (path: string, text: string): Set<string> => {
39 const found = new Set<string>()
40 const base = path.split('/').pop() ?? ''
41 const isRealEnv = base === '.env' || base === '.env.local'
42 if (PUBLIC_TOKEN.test(text)) found.add('public-token-env')
43 if (!isRealEnv && hasLiteralToken(text)) found.add('literal-token')
44 if (IS_CLIENT.test(text)) {
45 if (TOKEN_NAME.test(text)) found.add('token-in-client')
46 if (BROWSER_GATEWAY.test(text)) found.add('browser-gateway')
47 }
48 return found
49}
50
51// So o que a edicao introduz: um arquivo que ja tinha o problema nao trava o resto do trabalho nele.
52const introduced = (before: Set<string>, after: Set<string>) => [...after].filter(id => !before.has(id))
53
54const applyEdit = (text: string, oldString: string, newString: string, replaceAll?: boolean) => {
55 if (oldString === '') return text
56 if (replaceAll) return text.split(oldString).join(newString)
57 const i = text.indexOf(oldString)
58 return i < 0 ? text : text.slice(0, i) + newString + text.slice(i + oldString.length)
59}
60
61// O que imprime o segredo no transcript. Para checar se a variavel existe: [ -n "$ADAFLOW_APP_TOKEN" ] && echo set
62const PRINTS_TOKEN = [
63 /\b(?:echo|printf|cat)\b[^\n|;&]*\$\{?(?:ADAFLOW_APP_TOKEN|ADALINK_APP_TOKEN|ADA_TOKEN|ADAFLOW_JWT)\b/,
64 /\bprintenv\s+(?:ADAFLOW|ADALINK|ADA_)\w*/,
65 /\b(?:env|printenv|export\s+-p|set)\b[^\n]*\|\s*(?:grep|rg)\b[^\n]*(?:ada|token)/i,
66]
67
68const MAX_CHARS = 500_000
69
70// Motivo da recusa (ou undefined): puro, sem `$`; o efeito fica no ponto de chamada.
71const explain = (ids: string[], where: string) =>
72 ids.length === 0 ? undefined : `adaflow-guard (${ids.join(', ')}) em ${where}: ${ids.map(id => RULES[id]).join(' ')}`
73
74const TOKEN_PRINT_REASON =
75 'adaflow-guard (token-print): este comando imprimiria o app token no transcript. Para checar se a variavel existe use: [ -n "$ADAFLOW_APP_TOKEN" ] && echo definido'
76
77export const register: Register = (on, options) => {
78 const isDeny = String(options.mode) !== 'warn'
79
80 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
81 if (e.content.length > MAX_CHARS) return next(e)
82 const before = await $.fs.read(e.file_path).catch(() => '')
83 const reason = explain(introduced(violations(e.file_path, before), violations(e.file_path, e.content)), e.file_path)
84 if (reason) {
85 $.ui.log(reason, { to: 'debug' })
86 if (isDeny) return { deny: reason }
87 $.ui.toast(reason.slice(0, 120))
88 }
89 return next(e)
90 }).catch(($, e, next) => {
91 // Um erro do proprio guard nunca trava o trabalho: segue sem a verificacao e avisa.
92 $.ui.toast('adaflow-guard: erro interno, verificacao ignorada')
93 return next(e)
94 })
95
96 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
97 if (e.new_string.length > MAX_CHARS) return next(e)
98 const before = await $.fs.read(e.file_path).catch(() => '')
99 const after = applyEdit(before, e.old_string, e.new_string, e.replace_all)
100 const reason = explain(introduced(violations(e.file_path, before), violations(e.file_path, after)), e.file_path)
101 if (reason) {
102 $.ui.log(reason, { to: 'debug' })
103 if (isDeny) return { deny: reason }
104 $.ui.toast(reason.slice(0, 120))
105 }
106 return next(e)
107 }).catch(($, e, next) => {
108 $.ui.toast('adaflow-guard: erro interno, verificacao ignorada')
109 return next(e)
110 })
111
112 on('tool.call', { tool: 'Bash' }, ($, e, next) => {
113 if (PRINTS_TOKEN.some(re => re.test(e.command))) {
114 $.ui.log(TOKEN_PRINT_REASON, { to: 'debug' })
115 if (isDeny) return { deny: TOKEN_PRINT_REASON }
116 $.ui.toast('adaflow-guard: comando imprime o app token')
117 }
118 return next(e)
119 }).catch(($, e, next) => {
120 $.ui.toast('adaflow-guard: erro interno, verificacao ignorada')
121 return next(e)
122 })
123}
124