<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Cairo Labs]]></title><description><![CDATA[Cairo Labs]]></description><link>https://cairolabs.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad4cc1dd8cc1968658ce74a/ea66e852-2774-4994-9b1d-6a369eb94be2.png</url><title>Cairo Labs</title><link>https://cairolabs.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 12:34:47 GMT</lastBuildDate><atom:link href="https://cairolabs.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[CVE-2026-109618 e CVE-2026-109619 no PHP: guia prático para administradores]]></title><description><![CDATA[Aviso: a Cairo Labs não tem nenhuma afiliação com o PHP nem com The PHP Group. Este guia é um resumo em português das informações publicadas pelo próprio…]]></description><link>https://cairolabs.hashnode.dev/php-cve-2026-109618-109619-guia-pratico</link><guid isPermaLink="true">https://cairolabs.hashnode.dev/php-cve-2026-109618-109619-guia-pratico</guid><category><![CDATA[PHP]]></category><category><![CDATA[Segurança]]></category><dc:creator><![CDATA[Cairo Labs]]></dc:creator><pubDate>Wed, 21 Oct 2026 17:17:26 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Aviso:</strong> a Cairo Labs não tem nenhuma afiliação com o PHP nem com The PHP Group. Este guia é um resumo em português das informações publicadas pelo próprio time de segurança do PHP. Em caso de dúvida, vale o aviso oficial.</p>
</blockquote>
<h2>1. Resumo</h2>
<p>Em 21/10/2026, o time de segurança do PHP divulgou duas vulnerabilidades críticas de execução remota de código (RCE):</p>
<ul>
<li><p><strong>CVE-2026-109618</strong>: "RCE via OOM corruption".</p>
</li>
<li><p><strong>CVE-2026-109619</strong>: "RCE via stale global state".</p>
</li>
</ul>
<p>Ambas têm CVSS v4.0 <strong>9.2 (crítico)</strong> e não exigem autenticação. A complexidade de ataque é classificada como alta e há pré-requisitos.</p>
<p>Segundo o php.net, o problema atinge instalações PHP que atendem requests web (por exemplo PHP-FPM, Apache mod_php, php-cgi, FrankenPHP e LiteSpeed).</p>
<p><strong>O que fazer:</strong> atualizar o PHP para uma versão corrigida (ou para o pacote da sua distribuição que já inclua a correção). As mitigações temporárias da seção 6 reduzem o risco, mas <strong>não substituem a atualização</strong>.</p>
<h2>2. Links oficiais</h2>
<ul>
<li><p>Aviso e downloads: <a href="https://www.php.net/">https://www.php.net/</a></p>
</li>
<li><p>GHSA da CVE-2026-109618: <a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w">https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w</a></p>
</li>
<li><p>GHSA da CVE-2026-109619: <a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp">https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp</a></p>
</li>
<li><p>NVD CVE-2026-109618: <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-109618">https://nvd.nist.gov/vuln/detail/CVE-2026-109618</a></p>
</li>
<li><p>NVD CVE-2026-109619: <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-109619">https://nvd.nist.gov/vuln/detail/CVE-2026-109619</a></p>
</li>
<li><p>CVE.org CVE-2026-109618: <a href="https://www.cve.org/CVERecord?id=CVE-2026-109618">https://www.cve.org/CVERecord?id=CVE-2026-109618</a></p>
</li>
<li><p>CVE.org CVE-2026-109619: <a href="https://www.cve.org/CVERecord?id=CVE-2026-109619">https://www.cve.org/CVERecord?id=CVE-2026-109619</a></p>
</li>
<li><p>Anúncio na lista oss-security: <a href="https://www.openwall.com/lists/oss-security/2026/10/21/6">https://www.openwall.com/lists/oss-security/2026/10/21/6</a></p>
</li>
</ul>
<h2>3. Versões afetadas e corrigidas</h2>
<table>
<thead>
<tr>
<th>Ramo</th>
<th>Afetado</th>
<th>Corrigido em</th>
</tr>
</thead>
<tbody><tr>
<td>PHP 8.2</td>
<td>&lt; 8.2.35</td>
<td><strong>8.2.35</strong></td>
</tr>
<tr>
<td>PHP 8.3</td>
<td>&lt; 8.3.36</td>
<td><strong>8.3.36</strong></td>
</tr>
<tr>
<td>PHP 8.4</td>
<td>&lt; 8.4.27</td>
<td><strong>8.4.27</strong></td>
</tr>
<tr>
<td>PHP 8.5</td>
<td>&lt; 8.5.12</td>
<td><strong>8.5.12</strong></td>
</tr>
</tbody></table>
<p><strong>Nota sobre 7.4, 8.0 e 8.1:</strong> para esses ramos, o time do PHP publicou apenas <strong>backports mínimos em código-fonte, sem release oficial</strong>. Quem os usa precisa compilar com o backport ou obter a correção de um fornecedor de suporte estendido (LTS). O ideal é migrar para um ramo com release corrigida.</p>
<h2>4. Como checar a versão</h2>
<p><strong>Linha de comando</strong></p>
<pre><code class="language-bash">php -v
</code></pre>
<p>Atenção: a CLI pode ser uma versão diferente da usada pelo servidor web (FPM, Apache). Confira também o binário do serviço, por exemplo <code>php-fpm8.3 -v</code>.</p>
<p><strong>phpinfo(), com cuidado</strong></p>
<p>Se precisar ver a versão que o servidor web realmente usa, crie temporariamente um arquivo que chame <code>phpinfo()</code>, acesse-o e <strong>apague-o logo depois</strong>. Nunca deixe uma página de <code>phpinfo()</code> exposta publicamente: ela revela configuração interna do servidor. Se possível, restrinja o acesso por IP enquanto ela existir.</p>
<p><strong>Pacote da distribuição (Debian/Ubuntu)</strong></p>
<pre><code class="language-bash">dpkg -l | grep php
apt-cache policy php8.3     # troque 8.3 pelo seu ramo
</code></pre>
<p><strong>Changelog do pacote: o passo mais importante em distros</strong></p>
<p>As distribuições costumam aplicar a correção (backport) <strong>sem mudar o número da versão upstream</strong>. Então <code>php -v</code> pode mostrar uma versão "menor que a corrigida" e o pacote já estar protegido. Verifique no changelog:</p>
<pre><code class="language-bash">apt changelog php8.3-common | grep -i -E "CVE-2026-109618|CVE-2026-109619"
</code></pre>
<p>Se o CVE aparece como corrigido no changelog do pacote instalado, a correção está aplicada. Se não aparece, confira o aviso de segurança da sua distribuição. Em distros, <strong>vale o aviso de segurança da distro, não só o número da versão</strong>.</p>
<h2>5. Como atualizar</h2>
<h3>Ubuntu / Debian</h3>
<pre><code class="language-bash">sudo apt update
sudo apt upgrade
</code></pre>
<ul>
<li><p>Use os repositórios oficiais e acompanhe os avisos de segurança da sua distribuição para saber quando o pacote corrigido estiver disponível.</p>
</li>
<li><p>PPA <code>ondrej/php</code> e <code>packages.sury.org</code> são repositórios de <strong>terceiros</strong>. Se você os usa, a atualização depende do que eles publicarem; avalie se confia neles.</p>
</li>
<li><p>Depois de atualizar, <strong>reinicie o serviço</strong> (por exemplo <code>php8.3-fpm</code> ou o Apache) e confirme a versão/changelog novamente.</p>
</li>
</ul>
<h3>cPanel / EasyApache 4</h3>
<ul>
<li><p>Atualize os pacotes do EasyApache 4 (<code>ea-php*</code>) pelo WHM ou pelo gerenciador de pacotes do servidor.</p>
</li>
<li><p>No <strong>MultiPHP Manager</strong>, confira qual versão cada domínio usa e mova os sites para um ramo que receba a correção.</p>
</li>
<li><p>Se o seu ramo é 7.4, 8.0 ou 8.1, veja a nota da seção 3 e consulte seu fornecedor.</p>
</li>
</ul>
<h3>Docker</h3>
<ul>
<li><p>Use a imagem oficial <code>php:8.x</code>. As tags corrigidas (<code>8.2.35</code>, <code>8.3.36</code>, <code>8.4.27</code>, <code>8.5.12</code> e variantes) já estão no Docker Hub (conferido em 21/10/2026). Prefira fixar a tag exata, como <code>php:8.4.27-fpm</code>.</p>
</li>
<li><p>Atualize e refaça a imagem sem cache:</p>
</li>
</ul>
<pre><code class="language-bash">docker pull php:8.4.27-fpm   # ajuste ao seu ramo/variante
docker build --no-cache -t minha-app:novo .
</code></pre>
<ul>
<li><p>Faça o <strong>redeploy</strong> dos contêineres com a imagem nova e confirme a versão dentro do contêiner (<code>php -v</code>).</p>
</li>
<li><p>Imagens antigas já em execução <strong>não</strong> se atualizam sozinhas.</p>
</li>
</ul>
<h3>Hospedagem compartilhada</h3>
<ol>
<li><p>No painel, verifique a versão do PHP e, se houver seletor, troque para um ramo corrigido (8.2, 8.3, 8.4 ou 8.5).</p>
</li>
<li><p>Se o provedor ainda não oferece a versão corrigida, envie uma mensagem ao suporte. Modelo:</p>
</li>
</ol>
<blockquote>
<p>Olá, equipe de suporte. Foram divulgadas as vulnerabilidades CVE-2026-109618 e CVE-2026-109619 no PHP (aviso em <a href="https://www.php.net/">https://www.php.net/</a>). Gostaria de saber: (1) qual versão do PHP está ativa na minha conta/domínio; (2) se os servidores já receberam a versão corrigida (8.2.35, 8.3.36, 8.4.27 ou 8.5.12) ou o patch equivalente da distribuição; (3) previsão para a atualização, caso ainda não tenha sido feita. Obrigado.</p>
</blockquote>
<h2>6. Mitigações temporárias</h2>
<blockquote>
<p><strong>Fonte:</strong> as mitigações abaixo são as que constam no aviso do php.net (<a href="https://www.php.net/">https://www.php.net/</a>). Elas são <strong>incompletas</strong>: <strong>somente a atualização resolve.</strong></p>
</blockquote>
<ul>
<li><p><code>memory_limit=-1</code> definido via <code>php_admin_value</code>, (no pool do FPM ou na configuração do Apache), de modo que a aplicação não possa sobrescrever. Confira se nenhum pool, diretório ou aplicação volta a definir um limite finito. Segundo o aviso, isso remove a falha controlável do limite de memória, mas uma falha de alocação subjacente ainda pode chegar ao caminho vulnerável.</p>
</li>
<li><p><code>post_max_size</code> <strong>em 8M</strong> (ou <strong>32M</strong>, se você realmente precisa de uploads), com <code>upload_max_filesize</code> <strong>menor</strong> que <code>post_max_size</code>.</p>
</li>
<li><p><strong>Limites no proxy / servidor web</strong> para o tamanho da requisição decodificada, a quantidade de campos de entrada, as partes multipart, a quantidade de cabeçalhos e a profundidade de aninhamento, inclusive em requisições sem <code>Content-Length</code> utilizável.</p>
</li>
<li><p><strong>Limite de memória por cgroup/contêiner</strong>, como barreira de disponibilidade, de preferência com uma política que mate só o worker afetado. <strong>Não use</strong> <code>RLIMIT_AS</code> nem <code>RLIMIT_DATA</code>.</p>
</li>
<li><p><code>session.upload_progress.enabled=0</code>, definido em <code>php.ini</code>, na configuração do pool ou do servidor, e <strong>reiniciando os workers</strong>. Não funciona via <code>ini_set</code> nem via <code>.user.ini</code>.</p>
</li>
<li><p><strong>PHP 8.5:</strong> <code>fatal_error_backtraces=0</code> e reiniciar todos os workers (bloqueia só o caminho confirmado de backtrace fatal).</p>
</li>
<li><p><strong>Remover configurações INI inválidas ou obsoletas</strong> aplicadas por request que emitam diagnósticos antes da inicialização. Esconder diagnósticos com <code>error_reporting</code>, <code>display_errors</code> ou <code>@</code> não ajuda.</p>
</li>
<li><p><strong>Corrigir erros fatais e timeouts de execução que um atacante consiga provocar</strong>, inclusive em callbacks, no processamento de upload/importação e na limpeza.</p>
</li>
<li><p><code>session.upload_progress.enabled=0</code> bloqueia os caminhos de sessão confirmados, mas não outras variantes (php.net).</p>
</li>
</ul>
<p>Lembrete: nada disso substitui a atualização.</p>
<h2>7. Checklist final</h2>
<ul>
<li><p>[ ] Identifiquei todos os servidores, contêineres e hospedagens que rodam PHP.</p>
</li>
<li><p>[ ] Conferi a versão do PHP usada pelo serviço web (não só a CLI).</p>
</li>
<li><p>[ ] Para pacotes de distro, li o changelog (<code>apt changelog php8.x-common</code>) procurando os dois CVEs.</p>
</li>
<li><p>[ ] Atualizei para 8.2.35, 8.3.36, 8.4.27 ou 8.5.12 (ou pacote da distro com a correção).</p>
</li>
<li><p>[ ] Para 7.4, 8.0 ou 8.1: apliquei o backport em código-fonte, obtive correção de um fornecedor LTS ou planejei a migração.</p>
</li>
<li><p>[ ] Reiniciei PHP-FPM / Apache / workers após a atualização.</p>
</li>
<li><p>[ ] No Docker: <code>docker pull</code>, rebuild sem cache e redeploy concluídos.</p>
</li>
<li><p>[ ] Em hospedagem compartilhada: versão trocada no painel ou chamado aberto ao suporte.</p>
</li>
<li><p>[ ] Se a atualização não foi possível ainda: apliquei as mitigações da seção 6 e marquei uma data para atualizar.</p>
</li>
<li><p>[ ] Removi qualquer página <code>phpinfo()</code> criada para o diagnóstico.</p>
</li>
<li><p>[ ] Acompanho os avisos oficiais (php.net, GHSA e avisos da minha distribuição).</p>
</li>
</ul>
<hr />
<p><strong>Cairo Labs</strong> · 21/10/2026 Não temos afiliação com o PHP nem com The PHP Group.</p>
<p><em>Versão original e atualizada: <a href="https://claude-md-generator.pages.dev/php-cve/?utm%5C_source=hashnode-ptbr&amp;utm%5C_medium=artigo&amp;utm%5C_campaign=desafio1k">https://claude-md-generator.pages.dev/php-cve/?utm\_source=hashnode-ptbr&amp;utm\_medium=artigo&amp;utm\_campaign=desafio1k</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Contexto só quando importa: rules com paths:, skills e ${VAR} no Claude Code]]></title><description><![CDATA[.claude/rules com paths:, frontmatter de SKILL.md que define quem invoca o quê e variáveis no .mcp.json que não vazam nem quebram em silêncio, conferido na…]]></description><link>https://cairolabs.hashnode.dev/claude-code-rules-com-paths-skills-e-variaveis-no-mcp</link><guid isPermaLink="true">https://cairolabs.hashnode.dev/claude-code-rules-com-paths-skills-e-variaveis-no-mcp</guid><category><![CDATA[claude-code]]></category><category><![CDATA[inteligencia artificial ]]></category><category><![CDATA[programação]]></category><category><![CDATA[Desenvolvimento de Software]]></category><category><![CDATA[ferramentas]]></category><dc:creator><![CDATA[Cairo Labs]]></dc:creator><pubDate>Mon, 19 Oct 2026 13:39:53 GMT</pubDate><content:encoded><![CDATA[<p>No post anterior (em inglês: <a href="https://cairolabs.hashnode.dev/claude-md-is-advice-hooks-are-enforcement">CLAUDE.md Is Advice. Hooks Are Enforcement.</a>) eu defendi que o <code>CLAUDE.md</code> é conselho e os hooks são o que de fato obriga. Este aqui é sobre a outra metade de manter o <code>CLAUDE.md</code> curto: tirar instruções dele e colocar em lugares onde elas carregam <strong>só quando fazem sentido</strong>, e manter segredo fora da config que vai pro repositório.</p>
<p>Três recursos fazem quase todo esse trabalho:</p>
<ol>
<li><p><code>.claude/rules/*.md</code> <strong>com</strong> <code>paths:</code>: instruções que carregam quando o Claude mexe em arquivos que batem com o padrão.</p>
</li>
<li><p><strong>Skills (</strong><code>SKILL.md</code><strong>)</strong>: procedimentos e conhecimento com um frontmatter que decide <em>quem</em> pode invocar e <em>quando</em>.</p>
</li>
<li><p><code>${VAR}</code> <strong>no</strong> <code>.mcp.json</code>: config de MCP compartilhada, com os valores de cada máquina e os tokens ficando no ambiente de cada dev.</p>
</li>
</ol>
<p>Tudo aqui foi conferido na documentação do Claude Code (code.claude.com/docs) em outubro de 2026. A saída do <code>claude mcp list</code> mais abaixo é saída real do CLI v2.1.300. Quando algo vem da documentação e eu não rodei, eu aviso.</p>
<p><em>Transparência: eu faço o gerador grátis e o kit pago citados no fim. Texto escrito com ajuda de IA a partir da documentação oficial; as saídas de comando mostradas aqui são de comandos que eu rodei.</em></p>
<h2>1. Rules com escopo de caminho: <code>.claude/rules/</code> com <code>paths:</code></h2>
<p>Qualquer <code>.md</code> dentro de <code>.claude/rules/</code> é uma rule. A busca é recursiva, então subpastas como <code>frontend/</code> e <code>backend/</code> funcionam. Uma rule <strong>sem</strong> frontmatter carrega na inicialização, com a mesma prioridade do <code>.claude/CLAUDE.md</code>. Uma rule <strong>com</strong> o campo <code>paths:</code> só carrega quando o Claude trabalha com um arquivo que bate com o padrão:</p>
<pre><code class="language-markdown">---
paths:
  - "src/api/**/*.ts"
  - "tests/api/**/*.test.ts"
---
# API handlers

- Validate every request body with the shared schema helper before using it.
- Errors go through `toHttpError()`; never return raw exception messages.
</code></pre>
<h3>Quando uma rule com escopo carrega de verdade?</h3>
<p>Segundo a doc de memória, uma rule com <code>paths</code> carrega quando o Claude usa a ferramenta <strong>Read, Write ou Edit</strong> num arquivo que bate com o padrão, ou quando ele vê esse arquivo com um comando Bash que conta como leitura (tipo <code>cat</code> ou <code>head</code> num arquivo só). Ou seja, a rule aparece na primeira vez que o Claude abre <code>src/api/users.ts</code>, não no startup. A graça é justamente essa: suas convenções de API ficam fora do contexto enquanto o Claude está mexendo em CSS.</p>
<h3>Quatro detalhes que pegam</h3>
<p><strong>1. Coloque aspas nos globs.</strong> Em YAML, um valor sem aspas que começa com <code>*</code> é um <em>alias</em>, não uma string. Olha o que um parser YAML padrão faz com isso (PyYAML 6.0.3):</p>
<pre><code class="language-text">paths:\n  - **/*.ts        -&gt; ScannerError: while scanning an alias
paths:\n  - "**/*.ts"      -&gt; {'paths': ['**/*.ts']}
paths: src/**/*.ts, lib/**/*.ts -&gt; {'paths': 'src/**/*.ts, lib/**/*.ts'}
</code></pre>
<p>Por que isso importa: a doc diz que, se o frontmatter não fizer parse, o Claude Code <strong>ignora o frontmatter e carrega a rule como se ela não tivesse</strong> <code>paths</code>, ou seja, <em>sempre</em>. Um errinho transforma, sem aviso nenhum, uma rule com escopo numa rule global. Rode <code>claude --debug</code> para ver o erro de parse. (A forma de string separada por vírgula, da última linha, também é aceita.)</p>
<p><strong>2.</strong> <code>paths</code> <strong>é o único campo que uma rule lê.</strong> Qualquer outra chave no frontmatter (<code>description:</code>, <code>priority:</code>…) é ignorada <em>sem erro</em>. Não espere que ela faça alguma coisa.</p>
<p><strong>3. Brace expansion tem limite.</strong> <code>src/**/*.{ts,tsx}</code> vira dois padrões; <code>{a,b}/{c,d}/*.{ts,tsx}</code> vira oito. A lista <code>paths</code> inteira de uma rule divide um orçamento de 1.000 padrões expandidos e 4 MiB. Um padrão que estouraria esse limite é usado sem expandir, e chaves literais não batem com arquivo nenhum. E mais: <code>[</code> abre uma bracket expression; escape (<code>photos \[2024/**</code>) se o nome da pasta tiver mesmo um colchete.</p>
<p><strong>4. Rules em symlink apontando pra fora do repo têm restrição.</strong> Dá para fazer symlink de uma pasta de rules compartilhada para dentro de <code>.claude/rules/</code>, mas um alvo fora do seu diretório de trabalho é tratado como import externo: nada carrega até você aprovar imports externos no projeto, e depois disso carregam <strong>só as rules sem</strong> <code>paths</code>. Para rules pessoais que valem em todo lugar, use <code>~/.claude/rules/</code>; as rules de usuário carregam antes das do projeto e nenhuma sobrescreve a outra, então mantenha as duas coerentes.</p>
<h2>2. Skills: decida quem invoca o quê</h2>
<p>Uma skill é uma pasta com um <code>SKILL.md</code>. Skills de projeto ficam em <code>.claude/skills/&lt;nome&gt;/SKILL.md</code> (commite), as pessoais em <code>~/.claude/skills/&lt;nome&gt;/SKILL.md</code>. Os arquivos antigos em <code>.claude/commands/*.md</code> continuam funcionando e aceitam o mesmo frontmatter, <strong>menos</strong> <code>name</code> <strong>e</strong> <code>paths</code>, então coisa nova deve ir para skills.</p>
<h3>A description é um orçamento, não um README</h3>
<p>O Claude decide quando usar uma skill pela <code>description</code> (mais o <code>when_to_use</code>, opcional). A doc diz que o texto combinado é cortado em <strong>1.536 caracteres</strong> na listagem de skills. Coloque o gatilho e o caso de uso principal na primeira frase.</p>
<h3>Duas flags, direções opostas</h3>
<table>
<thead>
<tr>
<th>Frontmatter</th>
<th>Você pode rodar <code>/nome</code></th>
<th>O Claude pode invocar</th>
<th>Description no contexto</th>
</tr>
</thead>
<tbody><tr>
<td>(padrão)</td>
<td>sim</td>
<td>sim</td>
<td>sim</td>
</tr>
<tr>
<td><code>disable-model-invocation: true</code></td>
<td>sim</td>
<td>não</td>
<td>não (só carrega quando você invoca)</td>
</tr>
<tr>
<td><code>user-invocable: false</code></td>
<td>não (some do <code>/</code>)</td>
<td>sim</td>
<td>sim</td>
</tr>
</tbody></table>
<p>Use <code>disable-model-invocation: true</code> em tudo que tem efeito colateral: <code>/deploy</code>, <code>/release</code>, postar uma mensagem. Você não quer o Claude decidindo fazer deploy porque o código "parece pronto". Use <code>user-invocable: false</code> para conhecimento de fundo, tipo como funciona um módulo legado, que não faz sentido como comando.</p>
<h3>Skills também podem ter escopo de caminho</h3>
<p>Skills aceitam o mesmo campo <code>paths:</code> das rules: com ele, o Claude carrega a skill automaticamente só quando está trabalhando com arquivos que batem com o padrão. Isso casa bem com skills de conhecimento:</p>
<pre><code class="language-markdown">---
name: migrations-guide
description: How we write and review database migrations. Use when creating or editing files in db/migrations.
user-invocable: false
paths:
  - "db/migrations/**"
---
- Applied migrations are immutable; add a new one.
- Every migration needs a reversible down step or a comment explaining why not.
</code></pre>
<h3>Dados ao vivo com <code>!</code> e scripts empacotados com <code>${CLAUDE_SKILL_DIR}</code></h3>
<p>Uma linha que começa com <code>!`comando`</code> roda <strong>antes</strong> de a skill chegar ao Claude, e a saída substitui a linha. Para comandos de várias linhas, use um bloco cercado aberto com <code>```!</code>. Dois detalhes da doc: a forma inline só é reconhecida no começo da linha ou depois de espaço (<code>KEY=!`cmd`</code> fica literal), e a saída é inserida uma vez só, nunca reprocessada.</p>
<p><code>${CLAUDE_SKILL_DIR}</code> expande para a pasta da própria skill, tanto no corpo quanto nas regras Bash do <code>allowed-tools</code>. Usar a variável nos dois lugares deixa a skill rodar o script que vem junto com ela sem prompt de permissão:</p>
<pre><code class="language-markdown">---
name: release-notes
description: Draft release notes from merged commits since the last tag.
disable-model-invocation: true
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/commits-since-tag.sh *)
---
## Commits since the last tag
!`git describe --tags --abbrev=0`

Run `${CLAUDE_SKILL_DIR}/scripts/commits-since-tag.sh --format md` and group the
output into Features / Fixes / Internal. Don't invent entries.
</code></pre>
<p>Duas coisas que a galera entende errado sobre <code>allowed-tools</code>:</p>
<ul>
<li><p>Ele <strong>pré-aprova</strong>, não restringe. Todas as outras ferramentas continuam disponíveis sob as suas regras de permissão normais. (Para tirar ferramentas existe um campo separado, <code>disallowed-tools</code>.)</p>
</li>
<li><p>A liberação dura <strong>um turno</strong>: ela some quando você manda a próxima mensagem, mesmo que o texto da skill continue no contexto.</p>
</li>
</ul>
<p>Se você não pode liberar shell arbitrário em skills (por política da empresa, digamos), o setting <code>disableSkillShellExecution</code> troca todo comando <code>!</code> por <code>[shell command execution disabled by policy]</code>. Ele é mais útil em managed settings.</p>
<h2>3. <code>${VAR}</code> no <code>.mcp.json</code>: compartilhe a config, não o token</h2>
<p>Commite o <code>.mcp.json</code> para o time inteiro ter os mesmos servidores MCP, e deixe os valores que mudam por máquina (ou que são secretos) em variáveis de ambiente.</p>
<p><strong>Sintaxe:</strong> <code>${VAR}</code> e <code>${VAR:-default}</code>. <strong>Onde expande:</strong> <code>command</code>, <code>args</code>, <code>env</code>, <code>url</code> e <code>headers</code>.</p>
<pre><code class="language-json">{
  "mcpServers": {
    "docs": {
      "type": "http",
      "url": "${DOCS_MCP_URL:-https://example.invalid}/mcp",
      "headers": { "Authorization": "Bearer ${DOCS_MCP_TOKEN}" }
    }
  }
}
</code></pre>
<h3>Variável sem valor não quebra com barulho</h3>
<p>Se uma variável não tem valor nem default, a config <strong>carrega mesmo assim</strong>, e o texto <code>${VAR}</code> é usado do jeito que está. O Claude Code avisa. Aqui está o <code>claude mcp list</code> (CLI 2.1.300) num repo novo com esse arquivo e <code>DOCS_MCP_TOKEN</code> sem valor:</p>
<pre><code class="language-text">docs: ${DOCS_MCP_URL}/mcp (HTTP) - ⏸ Pending approval (run `claude` to approve)

MCP config diagnostics ⚠
[Contains warnings] Project config (shared via .mcp.json)
 └ [Warning] [docs] mcpServers.docs: Missing environment variables: DOCS_MCP_TOKEN
</code></pre>
<p>Três coisas para reparar:</p>
<ul>
<li><p>Só o <code>DOCS_MCP_TOKEN</code> aparece no aviso; o <code>DOCS_MCP_URL</code> tem default. Com <code>DOCS_MCP_TOKEN=abc</code> definido, o aviso sumiu.</p>
</li>
<li><p>A URL aparece <strong>por referência</strong> (<code>${DOCS_MCP_URL}</code>), não resolvida. A doc diz que <code>claude mcp list</code>, <code>claude mcp get</code> e <code>/mcp</code> mostram as referências <code>${VAR}</code> pelo nome, então colar essa saída numa issue não vaza valor nenhum.</p>
</li>
<li><p>O servidor está <strong>pendente de aprovação</strong>, que é o próximo ponto.</p>
</li>
</ul>
<h3>Servidor de projeto precisa de aprovação, e o repo não se aprova sozinho</h3>
<p>O Claude Code pergunta antes de usar servidores do <code>.mcp.json</code> de um projeto. Dá para achar que commitar <code>"enableAllProjectMcpServers": true</code> no <code>.claude/settings.json</code> pula essa etapa. Numa pasta não confiável, não pula: eu commitei exatamente esse arquivo no repo de teste e o <code>claude mcp list</code> continuou mostrando <code>⏸ Pending approval</code>. Isso bate com a doc: o setting é ignorado até você aceitar o diálogo de confiança do workspace rodando <code>claude</code> na pasta. (Para zerar suas escolhas depois: <code>claude mcp reset-project-choices</code>.)</p>
<h3>Mais duas pegadinhas de variável</h3>
<ul>
<li><p><code>CLAUDE_PROJECT_DIR</code> <strong>em</strong> <code>command</code><strong>/</strong><code>args</code> <strong>precisa de default.</strong> O Claude Code define essa variável no ambiente do <em>servidor</em>, não no dele próprio, então num <code>.mcp.json</code> de projeto escreva <code>${CLAUDE_PROJECT_DIR:-.}</code>. Configs de plugin substituem direto.</p>
</li>
<li><p><strong>Nomes de credencial são lidos como vazios.</strong> Na <code>url</code> e nos <code>headers</code> de um servidor remoto, nomes como <code>ANTHROPIC_API_KEY</code>, <code>ANTHROPIC_AUTH_TOKEN</code> e <code>NPM_TOKEN</code> são lidos como vazios de propósito (um <code>:-default</code> também é ignorado), o que normalmente acaba num <code>401</code>. Use um nome de variável seu. Falei disso no post anterior (em inglês).</p>
</li>
</ul>
<h2>4. Checagem de 5 minutos no seu repo</h2>
<ol>
<li><p><code>grep -rn "^ - \*" .claude/rules .claude/skills</code> acha globs sem aspas que começam com <code>*</code>. Coloque aspas.</p>
</li>
<li><p><code>claude --debug</code> e procure erros de parse de frontmatter em rules e skills.</p>
</li>
<li><p>Toda skill com efeito colateral deveria ter <code>disable-model-invocation: true</code>.</p>
</li>
<li><p>Leia a <code>description</code> de cada skill: o gatilho está na primeira frase?</p>
</li>
<li><p><code>claude mcp list</code> num clone novo: cada linha <code>Missing environment variables</code> é o bug do primeiro dia de alguém do time. Adicione um <code>:-default</code> ou documente a variável no README.</p>
</li>
</ol>
<h2>Fechando</h2>
<p>O <code>CLAUDE.md</code> deve guardar o que vale no repo inteiro. O resto pode esperar a hora em que importa: rules com <code>paths:</code> para convenções de arquivos específicos, skills para procedimentos (com a flag de invocação certa) e <code>${VAR}</code> para a config de MCP compartilhada nunca carregar um token.</p>
<p>Se você quer um <code>CLAUDE.md</code> curto e um bloco de permissões para começar e pendurar essas coisas, eu fiz um <a href="https://claude-md-generator.pages.dev/pt/?utm_source=hashnode-ptbr&amp;utm_medium=article&amp;utm_campaign=desafio1k">Gerador de CLAUDE.md</a> grátis. Ele roda no seu navegador e o que você digita não sai dele (a única requisição de rede é um contador de visitas anônimo).</p>
<hr />
<p><em>Transparência: eu sou o criador e vendedor do</em> <a href="https://pay.kiwify.com.br/ppuZGsg?src=hashnode-ptbr&amp;utm_source=hashnode-ptbr&amp;utm_medium=article&amp;utm_campaign=desafio1k"><em>Claude Code Power Kit</em></a> <em>(R$149 pela Kiwify, com Pix, garantia de 7 dias), então eu ganho com cada venda. Ele traz 6 rules com escopo de caminho, 14 skills e 9 configs de MCP que usam variáveis</em> <code>${CCPK_*}</code><em>, na linha deste post. O kit é em inglês (templates, subagents, skills, hooks e guias), com um guia curto em pt-BR. Texto escrito com ajuda de IA a partir da documentação oficial. Não afiliado à Anthropic; Claude e Claude Code são marcas da Anthropic.</em></p>
]]></content:encoded></item><item><title><![CDATA[CLAUDE.md Is Advice. Hooks Are Enforcement.]]></title><description><![CDATA[Keep CLAUDE.md short and move every rule that must hold into permissions and hooks, with tested bash + jq examples.]]></description><link>https://cairolabs.hashnode.dev/claude-md-is-advice-hooks-are-enforcement</link><guid isPermaLink="true">https://cairolabs.hashnode.dev/claude-md-is-advice-hooks-are-enforcement</guid><category><![CDATA[claude-code]]></category><category><![CDATA[claude]]></category><category><![CDATA[AI]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Cairo Labs]]></dc:creator><pubDate>Sun, 18 Oct 2026 13:55:46 GMT</pubDate><content:encoded><![CDATA[<p>Most of us start the same way with Claude Code: something goes wrong, so we add a line to <code>CLAUDE.md</code>. "Never force-push to main." "Don't touch <code>.env</code>." "Run the tests before you say you're done." A few weeks later the file is long, and the model follows most of it most of the time.</p>
<p>That "most" is the whole problem. The official memory docs are explicit that Claude treats <code>CLAUDE.md</code> as context, <em>not enforced configuration</em>, and they point you to a <code>PreToolUse</code> hook if you need to block an action regardless of what Claude decides.</p>
<p>So here is the split I now use:</p>
<ul>
<li><p><code>CLAUDE.md</code> <strong>is advice.</strong> Short, specific, about how the project works.</p>
</li>
<li><p><strong>Permissions and hooks are enforcement.</strong> Anything that must hold every time goes there.</p>
</li>
</ul>
<p>This post walks through both halves with small, working examples. Everything was checked against the Claude Code docs (code.claude.com/docs) in October 2026, CLI v2.1.300, and the shell snippets were run with bash + jq.</p>
<p><em>Disclosure: I'm the maker of the free generator and the paid kit mentioned at the end. This post was written with AI assistance against the official docs, and I tested the snippets myself.</em></p>
<h2>1. What CLAUDE.md is actually good for</h2>
<p>The docs suggest keeping each <code>CLAUDE.md</code> under 200 lines, because longer files cost context and <em>reduce adherence</em>. In practice, mine hold three things:</p>
<ul>
<li><p><strong>Commands:</strong> how to install, test a single file, lint, typecheck, build.</p>
</li>
<li><p><strong>Conventions:</strong> the things a new teammate would get wrong on day one.</p>
</li>
<li><p><strong>Gotchas:</strong> domain rules Claude can't infer from the code.</p>
</li>
</ul>
<p>Two features keep it short.</p>
<p><strong>Imports.</strong> <code>@path/to/file</code> pulls another file into context, so <code>@AGENTS.md</code> or <code>@docs/architecture.md</code> doesn't have to be pasted in. Import parsing skips code spans and fenced code blocks, so if you want to <em>mention</em> a path without importing it, wrap it in backticks: <code>`@README`</code> stays literal, while a bare <code>@README</code> is imported.</p>
<p><strong>Path-scoped rules.</strong> Put instructions that only matter for part of the repo in <code>.claude/rules/*.md</code> with a <code>paths:</code> frontmatter. They load only when Claude works with matching files:</p>
<pre><code class="language-markdown">---
paths:
  - "db/migrations/**"
  - "**/*.sql"
---
# Migrations

- A migration that has been applied anywhere is immutable. Add a new one.
- Read the generated SQL before accepting it.
</code></pre>
<p>That keeps your migration rules out of context while Claude is editing CSS.</p>
<p>Notice what's <em>not</em> in that list: "never", "always", "under no circumstances". If you catch yourself typing those in <code>CLAUDE.md</code>, that rule probably belongs in one of the next two sections.</p>
<h2>2. Permissions: deny &gt; ask &gt; allow</h2>
<p>Permission rules live in <code>.claude/settings.json</code> (project) or your user settings. The precedence is the first thing to internalize: rules are evaluated <strong>deny, then ask, then allow</strong>, and the first match in that order wins. Specificity doesn't matter, so an allow rule can't carve an exception out of a deny rule.</p>
<pre><code class="language-json">{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": ["Bash(git diff *)", "Bash(git status *)", "Bash(npm test *)"],
    "ask":   ["Bash(git push *)"],
    "deny":  ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
  }
}
</code></pre>
<p>Two gotchas that cost me time:</p>
<p><strong>The space before</strong> <code>*</code> <strong>is part of the rule.</strong> <code>Bash(ls *)</code> matches <code>ls</code> and <code>ls -la</code> but not <code>lsof</code>. <code>Bash(ls*)</code> matches <code>lsof</code> too. So write <code>Bash(git diff *)</code>, with the space.</p>
<p><strong>Project allow rules wait for workspace trust.</strong> <code>permissions.allow</code> entries in a project's <code>.claude/settings.json</code> grant capability, so Claude Code only applies them after you accept the workspace trust dialog for that folder. If your allow list "doesn't work" in a freshly cloned repo, that's the first thing to check. Deny and ask rules are restrictions, so they don't depend on that step.</p>
<p>Add the <code>$schema</code> line: the settings schema at json.schemastore.org gives you editor autocomplete and catches typos. The file above validates against it.</p>
<h2>3. Hooks: exit 2 means "no"</h2>
<p>Permission rules match patterns. Hooks run <em>your code</em>, so they can make decisions a glob can't. A hook is a command that gets the event as JSON on stdin. For <code>PreToolUse</code>, that includes <code>tool_name</code> and <code>tool_input</code> (for Bash, <code>tool_input.command</code>).</p>
<p>The contract you need to remember:</p>
<ul>
<li><p><strong>Exit 0:</strong> carry on.</p>
</li>
<li><p><strong>Exit 2:</strong> block. On a <code>PreToolUse</code> hook that blocks the tool call, and <strong>stderr is sent back to Claude</strong> as the reason, so write a message that tells it what to do instead.</p>
</li>
<li><p>Stderr from a hook that exits 0 is never shown to Claude.</p>
</li>
</ul>
<p>Here's a hook that blocks force-pushes to <code>main</code>/<code>master</code>:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
# .claude/hooks/guard-push.sh — PreToolUse (matcher: Bash)
set -uo pipefail
command -v jq &gt;/dev/null || { echo "guard-push: jq is required; blocking to be safe." &gt;&amp;2; exit 2; }

cmd=$(jq -r '.tool_input.command // empty')
[ -z "$cmd" ] &amp;&amp; exit 0

if grep -Eq 'git +push\b.*(--force|-f\b|\+[^ ]*:)' &lt;&lt;&lt;"$cmd" &amp;&amp;
   grep -Eq 'git +push\b.*[ :+](main|master)( |$)' &lt;&lt;&lt;"$cmd"; then
  echo "Blocked: force-push to main/master. Push a branch and open a PR instead." &gt;&amp;2
  exit 2
fi
exit 0
</code></pre>
<p>Two details matter more than the regex:</p>
<ol>
<li><p><strong>Fail closed.</strong> If <code>jq</code> is missing, a naive hook reads an empty command and happily exits 0, which means "allowed". For a security hook, a missing dependency should block, not wave everything through.</p>
</li>
<li><p><strong>Match the branch, not a substring.</strong> An early version of mine matched <code>\bmain\b</code>, which also blocked <code>git push --force origin feature/main-menu</code> (the <code>-</code> is a word boundary). Anchoring on a space, <code>:</code> or <code>+</code> before the branch name fixed it.</p>
</li>
</ol>
<h3>"Ask" instead of "block"</h3>
<p>Exit 2 is binary. When you want a human in the loop instead, <code>PreToolUse</code> can print JSON with a <code>permissionDecision</code> of <code>deny</code>, <code>ask</code> or <code>allow</code>:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
# .claude/hooks/ask-reset.sh — PreToolUse (matcher: Bash)
set -uo pipefail
cmd=$(jq -r '.tool_input.command // empty')
if grep -Eq 'git +reset +.*--hard' &lt;&lt;&lt;"$cmd"; then
  jq -n '{hookSpecificOutput: {
            hookEventName: "PreToolUse",
            permissionDecision: "ask",
            permissionDecisionReason: "git reset --hard discards uncommitted work."}}'
fi
exit 0
</code></pre>
<p>Note the <code>exit 0</code>: the decision travels in the JSON, not the exit code.</p>
<h3>Wiring them up</h3>
<p>Hooks are registered in the same settings file. <code>$CLAUDE_PROJECT_DIR</code> keeps the paths working from any subdirectory:</p>
<pre><code class="language-json">{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-push.sh" },
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ask-reset.sh" }
        ]
      }
    ]
  }
}
</code></pre>
<p>Remember to <code>chmod +x</code> the scripts.</p>
<h2>4. Stop hooks and the infinite loop</h2>
<p>A <code>Stop</code> hook runs when Claude is about to finish its turn. Exit 2 there <em>prevents</em> stopping: Claude gets your stderr and keeps working. That's exactly what you want for "don't finish while the tests are red", and it's also an easy way to build a loop: the tests still fail, Claude stops again, your hook blocks again…</p>
<p>The docs give you the escape hatch: Stop hooks receive <code>stop_hook_active</code>, which is <code>true</code> when Claude is already continuing because of a stop hook. Check it first. (Claude Code also caps this at eight consecutive continuations, but don't design around the cap.)</p>
<pre><code class="language-bash">#!/usr/bin/env bash
# .claude/hooks/stop-test-gate.sh — Stop. Opt-in via TEST_GATE_CMD.
set -uo pipefail
input=$(cat)
# Already continuing because of a Stop hook? Let it stop, or we loop.
[ "$(jq -r '.stop_hook_active // false' &lt;&lt;&lt;"$input")" = "true" ] &amp;&amp; exit 0
[ -z "${TEST_GATE_CMD:-}" ] &amp;&amp; exit 0

cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
if ! out=$(bash -c "$TEST_GATE_CMD" 2&gt;&amp;1); then
  echo "Tests are failing (\`$TEST_GATE_CMD\`). Fix them before finishing. Last lines:" &gt;&amp;2
  tail -n 20 &lt;&lt;&lt;"$out" &gt;&amp;2
  exit 2
fi
exit 0
</code></pre>
<p>Register it under <code>"Stop"</code> (no matcher) and enable it per project with something like <code>TEST_GATE_CMD="npm test"</code>. I make test gates opt-in on purpose: a slow suite on every single stop gets old fast, and a gate you resent is a gate you delete.</p>
<p>If you only want to <em>remind yourself</em> (say, about uncommitted changes) without forcing Claude to keep going, print <code>{"systemMessage": "..."}</code> and exit 0 instead.</p>
<h2>5. The <code>.mcp.json</code> env-var gotcha</h2>
<p>This one produces a confusing <code>401</code> with no warning. In a remote MCP server's <code>url</code> and <code>headers</code>, Claude Code reads a set of well-known credential variables as <strong>empty</strong>, on purpose, so a repo's <code>.mcp.json</code> can't ship your credentials to a server it names. Covered names include <code>ANTHROPIC_API_KEY</code>, <code>ANTHROPIC_AUTH_TOKEN</code>, cloud provider tokens, <code>HTTPS_PROXY</code> and <code>NPM_TOKEN</code>, and a <code>:-default</code> fallback on them is ignored.</p>
<p>The fix is to use a variable name of your own:</p>
<pre><code class="language-json">{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/",
      "headers": { "Authorization": "Bearer ${MYAPP_GITHUB_PAT}" }
    }
  }
}
</code></pre>
<p>Separately, an unset variable that <em>isn't</em> on that list is left as the literal <code>${VAR}</code> text, and Claude Code warns about it in <code>claude mcp list</code> and <code>/mcp</code>. So if a server fails to connect, check there first.</p>
<h2>6. Hooks are code, so test them</h2>
<p>You don't need a Claude Code session to test a hook. Feed it the JSON it would receive and assert on the exit code or the decision:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -uo pipefail
H=.claude/hooks; pass=0; fail=0
bash_in() { jq -nc --arg c "$1" '{hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:$c}}'; }

expect_exit() { # expect_exit &lt;code&gt; &lt;hook&gt; &lt;command&gt;
  bash_in "$3" | "$H/$2" &gt;/dev/null 2&gt;&amp;1; local rc=$?
  if [ "$rc" -eq "$1" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL $2: '$3' exited $rc, want $1"; fi
}

expect_exit 2 guard-push.sh 'git push --force origin main'
expect_exit 2 guard-push.sh 'git push -f origin master'
expect_exit 2 guard-push.sh 'git push origin +HEAD:main'
expect_exit 0 guard-push.sh 'git push origin main'
expect_exit 0 guard-push.sh 'git push --force origin feature/main-menu'

d=$(bash_in 'git reset --hard HEAD~1' | "$H/ask-reset.sh" | jq -r '.hookSpecificOutput.permissionDecision')
[ "$d" = ask ] &amp;&amp; pass=$((pass+1)) || { fail=$((fail+1)); echo "FAIL ask-reset: got '$d'"; }

echo '{"stop_hook_active":false}' | TEST_GATE_CMD=false "$H/stop-test-gate.sh" 2&gt;/dev/null
[ $? -eq 2 ] &amp;&amp; pass=$((pass+1)) || { fail=$((fail+1)); echo "FAIL: stop gate should block"; }
echo '{"stop_hook_active":true}' | TEST_GATE_CMD=false "$H/stop-test-gate.sh" 2&gt;/dev/null
[ $? -eq 0 ] &amp;&amp; pass=$((pass+1)) || { fail=$((fail+1)); echo "FAIL: stop gate should let go"; }

echo "$pass passed, $fail failed"; [ "$fail" -eq 0 ]
</code></pre>
<p>The <code>feature/main-menu</code> case exists because that false positive actually happened. Every time a hook surprises you, add the input as a test case. Run the harness in CI next to the schema check for <code>settings.json</code>.</p>
<p>An honest caveat: I've run these on Linux with bash + jq. Hooks are shell scripts, so <code>grep -E</code> and <code>sed</code> differences between GNU and BSD (macOS) can bite. Test on the platforms your team actually uses.</p>
<h2>7. Smaller things worth knowing</h2>
<ul>
<li><p><strong>Custom commands were merged into skills.</strong> <code>.claude/commands/deploy.md</code> and <code>.claude/skills/deploy/SKILL.md</code> both give you <code>/deploy</code>; existing command files keep working.</p>
</li>
<li><p><strong>Hooks need a POSIX shell.</strong> On Windows, run them through WSL or Git Bash; native PowerShell won't run these scripts.</p>
</li>
<li><p><strong>PostToolUse can't undo anything.</strong> The tool already ran; exit 2 there only shows stderr to Claude. Put prevention in <code>PreToolUse</code>.</p>
</li>
<li><p><strong>Keep the hook messages actionable.</strong> "Blocked" teaches nothing. "Blocked: force-push to main. Push a branch and open a PR instead" gives Claude a next step.</p>
</li>
</ul>
<h2>Wrap-up</h2>
<p>The pattern fits on one line: <strong>if breaking a rule would hurt, it doesn't belong in</strong> <code>CLAUDE.md</code><strong>.</strong> Put it in a deny rule if a pattern can express it, in a hook if it needs logic, and test the hook like any other code. That leaves <code>CLAUDE.md</code> short and focused on what it's good at, which is telling Claude how your project works.</p>
<p>If you want a starting point for the short <code>CLAUDE.md</code> plus a matching <code>permissions</code> block, I built a free <a href="https://claude-md-generator.pages.dev/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=desafio1k">CLAUDE.md Generator</a>. It runs in your browser and your inputs never leave it (the only network request is an anonymous visit counter).</p>
<hr />
<p><em>I'm the maker: if you'd rather not write all of this yourself, the</em> <a href="https://cairolabs.gumroad.com/l/claude-code-power-kit?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=desafio1k"><em>Claude Code Power Kit</em></a> <em>(US$29) bundles 14 stack templates, path-scoped rules, permission presets and 11 hook scripts built on the same ideas (including a force-push guard and an opt-in test gate), with a bash + jq test harness. Written with AI assistance against the official docs. Not affiliated with Anthropic; Claude and Claude Code are trademarks of Anthropic.</em></p>
]]></content:encoded></item></channel></rss>