Хуки в Claude Code - это shell-команды, HTTP-вызовы или короткие LLM-проверки, которые запускаются автоматически до или после действия Claude. Они решают то, с чем CLAUDE.md не справляется: жёстко блокируют опасные действия (удаление .env, force-push в main), форматируют код после правки, инжектят свежий контекст в начало сессии. Дальше - разбор 4 типов обработчиков, 31 события жизненного цикла, тест «когда правило идёт в hook», 7 готовых хуков под копипаст в .claude/settings.json и 3 ловушки, в которые я сам попадал.
Каждый день в Telegram-канале - что нового в вайб-кодинге: инструменты, разборы, ошибки. Подпишись, чтобы быть в курсе.
Что такое хуки в Claude Code?
Я полгода держал в CLAUDE.md правило «никогда не редактируй .env напрямую». Claude нарушал его раз в месяц. Не злонамеренно - просто забывал к концу длинной сессии. Один раз в .env уехал лишний токен, другой - переменная с дублирующим именем переписала рабочую.
Перенёс это правило в хук - забыть его стало невозможно. На каждой попытке отредактировать .env процесс Claude Code тормозит и возвращает мне ошибку. Это не подсказка, это блок.
Hooks - это заданные тобой shell-команды, HTTP-endpoints или LLM-prompts, которые автоматически запускаются в определённых точках жизненного цикла Claude Code.
Перевод: хук - это твоя команда, которую Claude Code запускает на конкретном событии. На каждом редактировании файла, запуске Bash, старте сессии, завершении субагента - есть событие, на которое можно повесить хук.
Зачем это нужно вайб-кодеру:
- Защитить файлы с секретами от случайной правки.
- Автоматически прогонять prettier/eslint после каждой генерации кода.
- Блокировать
git push --forceв main без явного разрешения. - Инжектить свежий контекст в начало каждой сессии.
- Логировать действия Claude для аудита.
CLAUDE.md решает контекст. Хуки решают действия. Это две разные сущности, и путать их - главная ошибка новичков.
Чем хуки отличаются от CLAUDE.md и скиллов?
Anthropic в документации явно проговаривает разницу между CLAUDE.md и хуками:
Содержимое CLAUDE.md приходит как user message после system prompt, а не как часть system prompt. Claude читает его и старается следовать, но строгого соблюдения гарантии нет.
Перевожу: контент CLAUDE.md приходит в Claude как первое сообщение от пользователя, а не как системная инструкция. Модель его прочитает и постарается выполнить. Но никаких гарантий нет - особенно к концу длинной сессии, когда контекст разрастается и старые инструкции вытесняются.
Хук работает иначе. Он живёт в settings.json, не зависит от модели и запускается процессом Claude Code на конкретном событии - PreToolUse, PostToolUse, SessionStart и других. Команда выполняется до того, как Claude получит результат, и может физически заблокировать действие (exit 2) или подправить файл после правки (exit 0).
Сравнение в таблице:
| Признак | CLAUDE.md | Hook | Skill |
|---|---|---|---|
| Где живёт | CLAUDE.md в корне проекта | .claude/settings.json | .claude/skills/ папка |
| Кто запускает | Модель читает как часть user message | Процесс Claude Code на событии | Модель через инструмент Skill |
| Гарантия выполнения | Нет (best-effort) | Да (детерминированный shell exit code) | Через вызов модели (с грантом, но не shell-уровня) |
| Что подходит | Контекст проекта, тон, стиль, маршрутизация | Запреты, форматирование, проверки на конкретных файлах | Сложные многошаговые процедуры с параметрами |
| Когда срабатывает | Каждое сообщение модели в сессии | На событии жизненного цикла | По вызову модели |
| Можно ли заблокировать действие | Нет | Да (exit 2) | Нет |
Полный шаблон CLAUDE.md и 6 правил живого файла - в гайде Как настроить CLAUDE.md в 2026: готовый шаблон и 6 правил. Скиллы как именованные навыки - в Claude Code Skills: как собрать библиотеку под себя.
Хук - это нижний уровень: без модели, без интерпретации, чистый shell.
Хочешь не только настроить хуки, но и собрать связку, которая делает Claude по-настоящему предсказуемым? Хуки - лишь один из трёх инструментов контекст-инжиниринга. На практикуме за 3 эфира собираешь все три кита: ИИ-клон (твоя методология в скиллах) + Второй мозг (структура знаний проекта) + контекст-инжиниринг (Plan Mode, /compact, хуки) - именно эта связка превращает Claude из «помощника с галлюцинациями» в надёжный инструмент.
4 типа хуков: Command, HTTP, Prompt, Agent - какой выбрать?
| Тип | Что делает | Когда выбрать | Сложность |
|---|---|---|---|
| Command | Запускает shell-команду, получает JSON на stdin, отвечает exit code и stdout | 90% случаев: проверка путей, форматирование, grep по содержимому, lsof, git-операции | низкая |
| HTTP | POST-запрос на твой URL с JSON в теле, такой же JSON в ответе | Когда правило живёт в команде/CI: централизованный валидатор, общая панель статусов, удалённый аудит | средняя (нужен сервер) |
| Prompt | Отправляет короткий промпт в Haiku (по умолчанию), получает yes/no для семантической оценки | Когда grep не справляется: «оцени, выглядит ли этот текст как корпоративный» | низкая |
| Agent | Создаёт субагента с доступом к Read/Grep/Glob для многошаговой проверки | Тяжёлая валидация: «найди все изменённые файлы и проверь, что есть соответствующие тесты» | высокая |
Самая частая ошибка - пытаться сделать на Command то, что нужно делать на Prompt. Пример: «не пиши длинные тире в .md файлах» - это grep, Command идеален. А «не пиши корпоративным тоном» - это семантика, нужен Prompt.
Если только начинаешь: бери Command. 7 готовых хуков ниже - все Command. HTTP/Prompt/Agent - следующий уровень, когда дойдёшь.
Где лежит settings.json и какие 4 уровня конфигов?
| Уровень | Путь | Когда применяется | В git? |
|---|---|---|---|
| Корпоративный | /Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux) | Через MDM на всю команду | нет |
| Пользовательский | ~/.claude/settings.json | Все проекты одного юзера | нет |
| Проектный | .claude/settings.json в корне репо | Этот проект, для всей команды | да |
| Локальный | .claude/settings.local.json | Только этот проект, только этот юзер | нет (в .gitignore) |
Я держу 7 описанных ниже хуков в проектном .claude/settings.json - чтобы при клонировании репозитория хуки подтянулись автоматически, и Claude в новом окне сразу работал по тем же правилам. Личные экспериментальные хуки - в .claude/settings.local.json, чтобы не мешать команде.
Все 4 файла мерджатся при старте сессии. Если правила противоречат - более низкий уровень побеждает (локальный важнее проектного, проектный важнее пользовательского).
Самый быстрый способ создать первый хук - интерактивное меню /hooks прямо в Claude Code. Выбираешь событие из списка, выбираешь matcher (например, Bash или Edit), вставляешь команду. Claude Code сам пишет валидный JSON в settings.json.
31 событие жизненного цикла: какие нужны новичку?
События бывают трёх частот: раз за сессию (SessionStart, SessionEnd), раз за turn (UserPromptSubmit, Stop, StopFailure) и на каждый tool call внутри agentic loop (PreToolUse, PostToolUse).
Три каденса важно понимать, чтобы не путаться:
- Раз за сессию -
SessionStart,SessionEnd,Setup. Запускаются один раз при открытии и закрытии Claude Code. Для инициализации контекста, очистки временных файлов. - Раз за ход -
UserPromptSubmit,UserPromptExpansion,Stop,StopFailure. Срабатывают на каждое твоё сообщение и на каждую остановку модели. Для логирования диалога, финальной проверки результата. - На каждый вызов инструмента -
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,PostToolBatch,SubagentStart,SubagentStop. Срабатывают на каждое редактирование файла, запуск Bash, чтение, поиск. Это самый горячий слой - где живут запреты, форматирование, валидации.
Полный список из 31 события включает асинхронные/реактивные: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. Это для продвинутых сценариев - пока пропусти.
Тест из 5 вопросов: когда правило идёт в hook, а когда остаётся в CLAUDE.md?
У меня это работает как закон, а не как пожелание. Я для себя свёл выбор между CLAUDE.md и хуком к простому тесту из пяти вопросов. Если хотя бы три ответа «да» - правило идёт в хук. Меньше трёх - остаётся в тексте.
Тест «когда правило идёт в hook»:
- Это правило про действие, а не про контекст? («не запускай
rm -rf» - действие; «у меня TypeScript-проект» - контекст) - Это правило про безопасность, деньги или данные? («не трогай
.env», «не публикуй ключи») - Можно проверить через grep, regex или exit code shell-команды? («после Edit
.mdпроверь что нет длинного тире») - Если правило нарушится, последствия будут болезненно отменять? («force push в main» - сложно откатить; «забыл точку в конце» - легко)
- Я уже видел, как Claude забывал это правило хотя бы один раз?
Пример: «не используй длинное тире, только дефис». Действие? Да. Безопасность? Нет. Проверяется grep? Да. Сложно отменить? Нет. Видел нарушение? Да. 3 из 5 - в хук.
Контр-пример: «у меня вайб-кодеры, не разработчики, пиши без слэнга». Действие? Скорее контекст. Безопасность? Нет. Проверяется регексом? Слишком сложно. Сложно отменить? Нет. Видел нарушение? Да. 1 из 5 - в CLAUDE.md.
Через тест прошли 5 моих правил. Они переехали в хуки. Остальные 75% - остались в текстовом файле.
7 готовых хуков под копипаст
Все 7 хуков хранятся в одном файле .claude/settings.json в корне проекта. Каждый - запись с типом события, фильтром по инструменту (matcher) и shell-командой. Если команда возвращает exit code 2 - действие модели блокируется, и Claude получает stderr как сообщение о причине.
Для большинства hook-событий действие блокирует только exit code 2. Exit code 1 Claude Code считает не блокирующей ошибкой и продолжает действие, хотя по Unix-конвенции 1 - это код провала.
exit 0 - прошло. exit 1 - ошибка, но Claude продолжит (это первый антипаттерн ниже). exit 2 - жёсткий стоп, расскажи Клоду что не так.
Хук 1. Защита .env от любых правок (PreToolUse, exit 2)
Блокирует попытку отредактировать любой файл вида .env, .env.local, .env.production напрямую.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Запрещено редактировать .env напрямую. Скажи мне какую переменную добавить, я сам открою файл.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}Что делает: перед каждым Edit/Write/MultiEdit берёт путь файла, проверяет grep'ом совпадает ли он с шаблоном .env, и если да - возвращает exit 2 с сообщением. Claude видит stderr и пишет мне: «попытался отредактировать .env, хук заблокировал, говори какую переменную добавить».
Хук 2. Авто-prettier после каждой правки кода (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
}
]
}
]
}
}Что делает: после каждого Edit/Write берёт путь, если расширение .ts/.tsx/.js/.jsx/.json - запускает prettier в фоне через &. exit 0 всегда, потому что это форматирование, а не запрет. Если prettier упадёт - ничего страшного, файл останется как был.
Разница с хуком 1 принципиальна: PreToolUse + exit 2 - жёсткий блок ДО действия. PostToolUse + exit 0 - мягкая правка ПОСЛЕ действия. Это два разных режима, под разные задачи.
Хук 3. Запрет git push --force в ветку main (PreToolUse, exit 2)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push в main запрещён. Сделай новую ветку или попроси меня вручную.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}Что делает: на каждый запуск Bash проверяет, что в команде есть push, есть --force/--force-with-lease/-f, и упоминается main. Если все три условия совпали - блок с понятным сообщением.
Раньше у меня в CLAUDE.md это правило было заглавными буквами. Claude всё равно нарушал примерно раз в две недели, когда я просил «зафорсись чтобы откатить мой неудачный коммит». Теперь это физически невозможно.
Хук 4. Блокировка длинного тире в текстовых файлах (PostToolUse, exit 2)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"В файле $FILE найдено длинное тире (U+2014). Замени на обычный дефис (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}Что делает: PostToolUse с exit 2 - это пост-проверка с блокировкой результата. Если Claude отредактировал файл и оставил длинное тире - хук падает, Claude получает обратную связь и пробует снова. Через 2-3 сессии Claude перестаёт ставить этот символ вообще.
Обрати внимание: grep ищет именно символ длинного тире (U+2014) через $'\xe2\x80\x94' - bash-форма записи через UTF-8 байты. Если просто написать grep -q '-' - будет ловить обычный дефис и срабатывать на каждом файле. Невидимый символ ломает всю логику.
Хук 5. Освободить порт 3000 перед npm run dev (PreToolUse, exit 0)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Порт 3000 освобождён перед запуском' >&2; }; exit 0"
}
]
}
]
}
}Что делает: перед каждым npm run dev (или pnpm/yarn) убивает процесс на порту 3000. exit 0 всегда, потому что это подготовка, а не запрет. Защищает от класса ловушек, когда Claude видит «порт занят», думает что проблема в коде и начинает чинить - а на деле просто старый сервер не закрылся.
Хук 6. Авто-коммит после успешных тестов (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); EXIT=$(jq -r '.tool_response.exit_code // 1'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+(run[[:space:]]+)?test' && [ \"$EXIT\" = \"0\" ] && { git add -A && git commit -m 'tests: автокоммит после успешного прогона' --no-verify 2>/dev/null; }; exit 0"
}
]
}
]
}
}Что делает: после каждого npm test / pnpm test / yarn test проверяет код возврата. Если exit_code = 0 (тесты прошли) - делает git add -A и коммит с понятным сообщением. exit 0 всегда. Чекпоинт автоматически - не нужно помнить про коммит.
Антипаттерн: ставить git commit --no-verify на каждый Edit. Это создаёт мусорные коммиты после каждой строчки кода. --no-verify нужен только потому что pre-commit хуки сами часто запускают тесты, и получится бесконечный цикл. Если у тебя нет pre-commit - убери --no-verify.
Хук 7. Инжект свежего контекста при старте сессии (SessionStart с additionalContext)
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Текущая ветка: $BRANCH. Незакоммиченные изменения:\\n$STATUS\\\"}\""
}
]
}
]
}
}Что делает: при старте сессии собирает текущую git-ветку и список незакоммиченных изменений, возвращает JSON с полем additionalContext. Claude Code инжектит этот текст в первое сообщение системы - Claude знает с самого начала, где он находится, что осталось от прошлой сессии.
Используй additionalContext для информации, которую Claude должен знать о текущем состоянии окружения или о только что выполненной операции: состояние окружения (текущая ветка, цель деплоя, активные feature flags), условные правила проекта (какая test-команда применяется к только что отредактированному файлу), внешние данные (открытые issues на тебе, недавние результаты CI).
Расширения этого хука: подтянуть последний коммит и его описание, статус GitHub Actions, активный спринт из Linear/Jira, открытые issues по текущему репозиторию.
3 ловушки хуков, в которые попадает каждый второй
Ловушка 1. Хук с exit 1 молча проходит
Самая опасная. Я неделю думал, что мой .env-protector работает, а оказалось - у меня в хуке был jq -r '.tool_input.file_path' без проверки на null. Когда tool_input был пустой, jq падал с exit 1. Claude видел exit 1 как «ошибка хука, продолжаем» - действие проходило. Долго не понимал, почему .env всё-таки трогается. Только когда вручную запустил хук в терминале с пустым входом - увидел причину.
Лечится одним правилом: в каждом хуке начинай с проверки на null, и в конце ставь || exit 0. Любой неожиданный выход даст «безопасный» exit, а не молчаливое прохождение.
# Шаблон безопасного начала и конца:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... твоя логика ...
exit 0 # или || exit 0 в одну строкуЛовушка 2. Медленные хуки убиваются по таймауту
У меня был хук, который запускал eslint --fix на весь файл после Edit. Большие файлы (1000+ строк) обрабатывались по 8-10 секунд. Стандартный таймаут хука в Claude Code - 60 секунд, я в него вроде вписывался. Но когда несколько Edit идут подряд за минуту - хуки накапливаются, стоят в очереди, и часть просто пропускается. Понял это, только когда в логе вышло «hook timed out».
Лечится так: если хук требует тяжёлой работы, выноси в фон. eslint --fix "$FILE" & без ожидания результата. Тогда хук возвращает exit 0 за миллисекунды, а реальная работа крутится в background.
# Тяжёлая операция в background:
npx eslint --fix "$FILE" &
exit 0Ловушка 3. Хук не видит переменные окружения проекта
Я думал, что process.env в хуке доступен как в Node.js-приложении. Нет. Хук запускается под shell от родителя Claude Code и видит только то, что есть в ~/.zshrc или ~/.bashrc, плюс то, что Claude Code пробрасывает явно. Если твой хук зависит от OPENAI_API_KEY из .env - придётся читать .env вручную.
Лечится: явное чтение .env в команде хука.
# Прокидывание env проекта в hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Или через dotenv:
dotenv -e .env -- node my-validator.jsОсобенно больно в монорепо, где разные пакеты имеют свои .env. Решается прокидыванием PWD и явным cd "$PWD" в начале команды.
Как тестировать хуки до коммита?
Базовый сценарий теста. Берёшь команду хука из JSON, открываешь терминал и подаёшь ей на stdin тот самый JSON, который Claude Code прислал бы:
# Тест хука 1 (защита .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
echo "BLOCKED" >&2
exit 2
} || exit 0
'
echo "Exit code: $?"Ожидаешь BLOCKED в stderr и Exit code: 2. Если получил exit 0 - твой регекс не работает, чини grep.
Сценарии для проверки:
- Happy path - файл должен заблокироваться.
.envдля хука 1,git push --force mainдля хука 3. - Negative path - файл не должен заблокироваться.
README.mdдля хука 1,git push origin feature-branchдля хука 3. - Edge cases - пустой input, null значения, неожиданные символы. Хук должен либо ответить exit 0, либо упасть с понятной ошибкой - не молча проходить.
Где складывать экспериментальные хуки. Не в .claude/settings.json, а в .claude/settings.local.json (не в git). Туда складываешь новые хуки в PostToolUse с echo вместо реального действия, смотришь срабатывают ли вообще, потом переписываешь в боевой settings.json.
// Эксперимент в settings.local.json - пишет в /tmp/hook-debug.log:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
}
]
}
]
}
}После 5-10 правок в Claude Code открываешь /tmp/hook-debug.log - видишь, на каких файлах хук сработал. Если пусто - matcher не подходит, чини регекс.
Что дальше?
5 шагов на ближайшую неделю:
- Прогрепай свой
CLAUDE.mdпо словам «никогда», «запрещено», «всегда», «обязательно». Каждое такое правило - кандидат в хук. Если правило императивное, текст в файле его не удержит. - Начинай с PostToolUse + exit 0. Это самые безопасные - что-то поправляют, ничего не блокируют. Prettier (хук 2), убийство dev-сервера (хук 5), авто-коммит после тестов (хук 6).
- Только когда уверен - добавь первый PreToolUse + exit 2. Например, защиту
.env(хук 1) или запрет force-push (хук 3). Это уже жёсткий блок, ошибиться в команде grep = заблокировать самого себя. - Тестируй хуки вручную перед коммитом (раздел выше). Так экономишь часы отладки.
- Держи
settings.local.jsonдля экспериментов. Туда новые хуки с echo вместо реального действия.
После того как хуки настроены - следующий шаг кластера: как откатить изменения в Claude Code, если хук всё-таки пропустил что-то опасное. Если хочешь, чтобы Claude помнил структуру проекта между сессиями - собери Второй мозг в Claude Code (папка business/ + связанная база знаний). А чтобы навыки твоего проекта были именованы и переиспользуемы - Claude Code Skills.
Источники
- Claude Code Hooks reference - официальная документация Anthropic
- Claude Code Memory documentation - про CLAUDE.md и системный промпт
- Automate workflows with hooks - официальный гайд Anthropic с примерами
- Claude Code Hooks: Complete Guide to All 12 Lifecycle Events - claudefa.st
- Claude Code Hooks: 6 Production Patterns - pixelmojo.io
- Claude Code Hooks Tutorial: 5 Production Hooks From Scratch - Blake Crosley
- Claude Code Hooks: A Practical Guide to Workflow Automation - DataCamp
- Claude Code Hooks: 20+ Ready-to-Use Examples - DEV.to / Lukasz Fryc
- Claude Code Hooks Mastery - disler/claude-code-hooks-mastery (GitHub)
- Claude Code: маршрут обучения 2026 - Хабр
- Claude Code: практический гайд по настройке - Хабр
- 44 настройки Claude Code - Хабр
Полная схема по вайб-кодингу за вечер: ИИ-клон + Второй мозг + Контекст-инжиниринг. 3 эфира, 2 000 ₽. Записи остаются у тебя.
Новые материалы - дайджестом, без спама
Гайды выходят регулярно. Подпишись, чтобы не пропускать: пришлю подборку в Telegram или на email. Раз в неделю или каждый день - выбираешь сам.

