Certifikační otázka má jedinou správnou odpověď ze čtyř. I když ji trefíš, tvůj .env je pořád čitelný.
Otázka z certifikace a rychlá odpověď
Zadání kvízu zní zhruba takhle: „Jak zabráníš Claude Code, aby přečetl obsah .env souborů?" Nabídnuté odpovědi:
- A) PostToolUse hook, matching Write and Edit.
- B) PreToolUse hook, matching Write and Create.
- C) PreToolUse hook, matching Read and Grep.
- D) PostToolUse hook, matching Read and Delete.
Správně je C – jediný PreToolUse hook, který cílí na čtecí tooly. V produkci ti ale samotný matcher Read|Grep .env neuzavře.
Proč A, B, D padají
A je PostToolUse. Tool už doběhl, obsah .env je v tool result a model ho viděl. Blokovat Write a Edit navíc chrání proti zápisu, ne proti čtení. Dvojitá chyba.
B zní dobře, dokud si neuvědomíš, že tool Create v Claude Code neexistuje. Nové soubory vznikají přes Write. A Write stejně neřeší čtení.
D má obě chyby. Delete neexistuje (mažeš přes Bash rm) a PostToolUse přichází pozdě. Kvíz tu mimoděk testuje, jestli znáš reálný inventář toolů: Read, Write, Edit, NotebookEdit, Glob, Grep, Bash, Task, WebFetch, WebSearch, TodoWrite, BashOutput, KillShell, SlashCommand, ExitPlanMode. To je základní inventář. MCP servery seznam dynamicky rozšiřují, takže matcher kontroluj proti tomu, co v projektu reálně běží.
PreToolUse hook v jedné minutě
PreToolUse běží před vykonáním toolu. Hook skript dostane na stdin JSON s session_id, transcript_path, cwd, hook_event_name, tool_name a tool_input. Exit 0 pustí tool dál, 2 ho zablokuje a stderr jde modelu jako feedback. Cokoli jiného je chyba hooku a tool poběží dál.
Matcher v settings.json je regex nad tool_name. "Read|Grep|Glob" trefí všechny tři jednou konfigurací. Glob přidávám záměrně: samotný Glob .env* sice obsah nepřečte, ale prozradí modelu, že soubor existuje. To modelu stačí, aby se o čtení pokusil jinou cestou. Detaily ke všem devíti eventům (PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd) najdeš v kompletním průvodci hooks.
Funkční hook pro Read, Grep a Glob
Hook dáš do .claude/settings.json v projektu (committable) nebo do ~/.claude/settings.json (user-level):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep|Glob",
"hooks": [
{ "type": "command", "command": ".claude/hooks/block-env.sh" }
]
}
]
}
}
A skript .claude/hooks/block-env.sh:
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[ -z "$path" ] && exit 0
resolved=$(realpath -m "$path" 2>/dev/null || echo "$path")
base=$(basename "$resolved")
case "$base" in
.env|.env.*)
echo "Blocked: $resolved matches .env policy. Use a vault or ask the user." >&2
exit 2
;;
esac
exit 0
Na čem ti hook spadne: Read používá file_path, Grep a Glob path (i pattern), takže parsuj oba. Bez realpath tě obejde ln -s .env public.txt – symlink je klasický bypass. A matchuj na basename, ne na celou cestu, jinak ti proteče apps/api/.env nebo packages/db/.env.production v monorepu.
Díra, kterou kvíz nezmíní: Bash
Matcher Read|Grep|Glob nechytne nic z tohohle:
cat .env
source .env
grep SECRET .env
export $(cat .env | xargs)
tail -f .env.production
awk '{print}' .env
Každý z těch příkazů jede přes tool Bash, ne Read. Model je použije, pokud uživatel řekne „načti proměnné" a nikdo nevysvětlil, že to nesmí. Druhá větev matcheru je povinná:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep|Glob",
"hooks": [
{ "type": "command", "command": ".claude/hooks/block-env.sh" }
]
},
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": ".claude/hooks/block-env-bash.sh" }
]
}
]
}
}
Bash varianta čte tool_input.command a jede regexem:
#!/usr/bin/env bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq '(\.env($|\.[a-zA-Z0-9_.-]+)|/\.env($|\.))'; then
if echo "$cmd" | grep -Eq '(cat|less|more|head|tail|grep|awk|sed|source|\.\s|xxd|od|base64|export)\b'; then
echo "Blocked: Bash command reads .env. Reject or refactor." >&2
exit 2
fi
fi
exit 0
Počítej s tím, že regex občas trefí i nevinný příkaz – komentář, log řádek nebo import cestu, která jen obsahuje řetězec .env. Pro ten případ si nech otevřený bypass přes .claude/settings.local.json nebo prostě příkaz reformulovat. Laťka se zvedne z „jeden řádek" na „musíš to chtít".
Co i se dvěma větvemi neuchytíš
Regex nad command prohraje s interpretry:
node -e "console.log(require('fs').readFileSync('.env','utf8'))"
python -c "print(open('.env').read())"
ruby -e "puts File.read('.env')"
perl -e 'print <>' < .env
Můžeš přidat regexy na node -e, python -c, ruby -e, perl -e, ale to je závod, který nevyhraješ. Pojistky, které dávají smysl vedle hooku:
permissions.denyvsettings.json– deklarativní vrstva, aktuální syntaxRead(./.env),Read(./.env.*),Bash(cat:*). Méně flexibilní než hook, ale čitelné v review.- Filesystem permissions –
.envvlastněný jiným uživatelem, Claude proces nemá read bit. - Rotace secretů a vault (1Password CLI, doppler, sops) –
.envv repu prostě není. .gitignorenechrání přístup – Claude Code si ho nečte. Pro blokaci čtení používejpermissions.denynebo hooks.
Certifikát ti dá bod za C. .env ochrání až hook, který ví o Bashi, symlinkách i vnořeném .env.production. I tak ti zbyde jeden interpretr, kterému musíš věřit.
Celou mapu hooks – devět eventů, exit codes, JSON I/O, security pitfalls – mám v průvodci Claude Code hooks.
Dělám workshopy a code review hooks/permissions setupů pro engineering týmy. Napiš na me@jakubkontra.com.