رفتن به محتوا

خودکارسازی کارها با hooks

Hooks دستورهای شِلِ تعریف‌شده توسط کاربرند که در نقاط مشخصی از چرخه‌ی عمرِ Claude Code اجرا می‌شوند. آن‌ها کنترلی قطعی روی رفتار Claude Code می‌دهند و تضمین می‌کنند که برخی کارها همیشه انجام شوند، به‌جای اینکه به انتخابِ LLM برای اجرای آن‌ها متکی باشی. از hooks برای اعمالِ قواعدِ پروژه، خودکارسازیِ کارهای تکراری، و یکپارچه‌سازیِ Claude Code با ابزارهای موجودت استفاده کن.

برای تصمیم‌هایی که به‌جای قواعدِ قطعی نیاز به قضاوت دارند، می‌توانی از hooksِ مبتنی‌بر پرامپت یا hooksِ مبتنی‌بر ایجنت استفاده کنی که برای ارزیابیِ شرط‌ها از یک مدلِ Claude بهره می‌گیرند.

برای راه‌های دیگرِ گسترشِ Claude Code، skills را برای دادنِ دستورها و فرمان‌های اجراییِ بیشتر به Claude، subagents را برای اجرای کارها در کانتکست‌های ایزوله، و plugins را برای بسته‌بندیِ افزونه‌ها جهتِ هم‌رسانی بینِ پروژه‌ها ببین.

اولین hookِ خود را راه‌اندازی کن

Section titled “اولین hookِ خود را راه‌اندازی کن”

برای ساختِ یک hook، یک بلاک hooks به یک فایل تنظیمات اضافه کن. این راهنمای گام‌به‌گام یک hookِ اعلانِ دسکتاپ می‌سازد تا هر وقت Claude منتظرِ ورودیِ توست به‌جای تماشای ترمینال، باخبر شوی.

hook را به تنظیماتت اضافه کن

فایل ~/.claude/settings.json را باز کن و یک hookِ Notification اضافه کن. مثالِ زیر برای macOS از osascript استفاده می‌کند؛ برای دستورهای Linux و Windows به وقتی Claude نیاز به ورودی دارد باخبر شو مراجعه کن.

{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}

اگر فایل تنظیماتت از قبل کلیدِ hooks دارد، Notification را به‌عنوان هم‌ترازِ کلیدهای رویدادِ موجود اضافه کن، نه اینکه کلِ شیء را جایگزین کنی. هر نامِ رویداد یک کلید داخلِ تک‌شیءِ hooks است:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
}
],
"Notification": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }]
}
]
}
}

همچنین می‌توانی با توصیفِ آنچه می‌خواهی در CLI، از Claude بخواهی hook را برایت بنویسد.

پیکربندی را تأیید کن

/hooks را تایپ کن تا مرورگرِ hooks باز شود. فهرستی از همه‌ی رویدادهای hookِ موجود می‌بینی، با شماره‌ای کنارِ هر رویداد که hook پیکربندی‌شده دارد. Notification را انتخاب کن تا تأیید کنی hookِ جدیدت در فهرست ظاهر می‌شود. انتخابِ hook جزئیاتش را نشان می‌دهد: رویداد، matcher، نوع، فایلِ منبع، و دستور.

hook را آزمایش کن

Esc را بزن تا به CLI برگردی. از Claude بخواه کاری انجام دهد که به مجوز نیاز دارد، سپس از ترمینال فاصله بگیر. باید یک اعلانِ دسکتاپ دریافت کنی.

چه چیزهایی را می‌توانی خودکار کنی

Section titled “چه چیزهایی را می‌توانی خودکار کنی”

hookها به تو امکان می‌دهند کد را در نقاطِ کلیدیِ چرخه‌ی عمرِ Claude Code اجرا کنی: قالب‌بندیِ فایل‌ها پس از ویرایش، مسدودکردنِ دستورها پیش از اجرا، فرستادنِ اعلان وقتی Claude نیاز به ورودی دارد، تزریقِ کانتکست در شروعِ نشست، و موارد بیشتر. برای فهرستِ کاملِ رویدادهای hook، به مرجع Hooks مراجعه کن.

هر مثال شامل یک بلاکِ پیکربندیِ آماده‌ی استفاده است که به یک فایل تنظیمات اضافه می‌کنی. رایج‌ترین الگوها:

برای نمونه‌ای production از hookهایی که یک بازبینیِ جداگانه با مدل اجرا می‌کنند و یافته‌ها را به نشست بازمی‌گردانند، به نحوه‌ی یکپارچه‌شدنِ پلاگینِ security-guidance با Claude Code مراجعه کن.

وقتی Claude نیاز به ورودی دارد باخبر شو

Section titled “وقتی Claude نیاز به ورودی دارد باخبر شو”

هر وقت Claude کارش را تمام کرد و به ورودیِ تو نیاز داشت، یک اعلانِ دسکتاپ بگیر، تا بتوانی بدونِ بررسیِ ترمینال به کارهای دیگر بپردازی.

این hook از رویدادِ Notification استفاده می‌کند که وقتی Claude منتظرِ ورودی یا مجوز است، فعال می‌شود. هر تبِ زیر از دستورِ اعلانِ بومیِ آن پلتفرم استفاده می‌کند. این را به ~/.claude/settings.json اضافه کن:

{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
اگر هیچ اعلانی ظاهر نشد

osascript اعلان‌ها را از طریقِ اپِ داخلیِ Script Editor مسیریابی می‌کند. اگر Script Editor مجوزِ اعلان نداشته باشد، دستور بی‌سروصدا شکست می‌خورد و macOS از تو نمی‌خواهد آن را اعطا کنی. این را یک‌بار در Terminal اجرا کن تا Script Editor در تنظیماتِ اعلانت ظاهر شود:

Terminal window
osascript -e 'display notification "test"'

هنوز چیزی ظاهر نمی‌شود. به System Settings > Notifications برو، Script Editor را در فهرست پیدا کن، و Allow Notifications را روشن کن. دستور را دوباره اجرا کن تا تأیید کنی اعلانِ آزمایشی ظاهر می‌شود.

matcherِ خالی روی همه‌ی انواعِ اعلان فعال می‌شود. برای فعال‌شدن فقط روی رویدادهای مشخص، آن را به یکی از این مقادیر تنظیم کن:

Matcherچه وقت فعال می‌شود
permission_promptClaude نیاز دارد تو یک استفاده‌ی ابزار را تأیید کنی
idle_promptClaude کارش تمام شده و منتظرِ پرامپتِ بعدیِ توست
auth_successاحراز هویت کامل می‌شود
elicitation_dialogیک سرورِ MCP یک فرمِ elicitation باز می‌کند
elicitation_completeیک فرمِ elicitationِ MCP ارسال یا رد می‌شود
elicitation_responseیک پاسخِ elicitationِ MCP به سرور بازفرستاده می‌شود

/hooks را تایپ کن و Notification را انتخاب کن تا تأیید کنی hook ثبت شده است. برای اسکیمای کاملِ رویداد، به مرجع Notification مراجعه کن.

قالب‌بندیِ خودکارِ کد پس از ویرایش‌ها

Section titled “قالب‌بندیِ خودکارِ کد پس از ویرایش‌ها”

به‌طور خودکار Prettier را روی هر فایلی که Claude ویرایش می‌کند اجرا کن، تا قالب‌بندی بدونِ مداخله‌ی دستی یکدست بماند.

این hook از رویدادِ PostToolUse با matcherِ Edit|Write استفاده می‌کند، بنابراین فقط پس از ابزارهای ویرایشِ فایل اجرا می‌شود. دستور، مسیرِ فایلِ ویرایش‌شده را با jq استخراج می‌کند و به Prettier می‌دهد. این را به .claude/settings.json در ریشه‌ی پروژه‌ات اضافه کن:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}

مسدودکردنِ ویرایشِ فایل‌های محافظت‌شده

Section titled “مسدودکردنِ ویرایشِ فایل‌های محافظت‌شده”

از تغییرِ فایل‌های حساس مثل .env، package-lock.json، یا هر چیزی در .git/ توسط Claude جلوگیری کن. Claude بازخوردی دریافت می‌کند که توضیح می‌دهد چرا ویرایش مسدود شد، تا بتواند رویکردش را تنظیم کند.

این مثال از یک فایلِ اسکریپتِ جداگانه استفاده می‌کند که hook آن را صدا می‌زند. اسکریپت مسیرِ فایلِ هدف را در برابرِ فهرستی از الگوهای محافظت‌شده بررسی می‌کند و با کدِ ۲ خارج می‌شود تا ویرایش را مسدود کند.

اسکریپتِ hook را بساز

این را در .claude/hooks/protect-files.sh ذخیره کن:

protect-files.sh
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0

اسکریپت را اجراشدنی کن (macOS/Linux)

اسکریپت‌های hook باید اجراشدنی باشند تا Claude Code بتواند آن‌ها را اجرا کند:

Terminal window
chmod +x .claude/hooks/protect-files.sh

hook را ثبت کن

یک hookِ PreToolUse به .claude/settings.json اضافه کن که اسکریپت را پیش از هر فراخوانیِ ابزارِ Edit یا Write اجرا کند:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}

تزریقِ دوباره‌ی کانتکست پس از فشرده‌سازی

Section titled “تزریقِ دوباره‌ی کانتکست پس از فشرده‌سازی”

وقتی پنجره‌ی کانتکستِ Claude پر می‌شود، فشرده‌سازی گفت‌وگو را خلاصه می‌کند تا فضا آزاد شود. این کار می‌تواند جزئیاتِ مهم را از دست بدهد. از یک hookِ SessionStart با matcherِ compact استفاده کن تا کانتکستِ حیاتی را پس از هر فشرده‌سازی دوباره تزریق کنی.

هر متنی که دستورت روی stdout بنویسد به کانتکستِ Claude افزوده می‌شود. این مثال قراردادهای پروژه و کارهای اخیر را به Claude یادآوری می‌کند. این را به .claude/settings.json در ریشه‌ی پروژه‌ات اضافه کن:

{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}

می‌توانی echo را با هر دستوری که خروجیِ پویا تولید می‌کند جایگزین کنی، مثل git log --oneline -5 برای نمایشِ کامیت‌های اخیر. برای تزریقِ کانتکست در هر شروعِ نشست، به‌جایش CLAUDE.md را در نظر بگیر. برای متغیرهای محیطی، به CLAUDE_ENV_FILE در مرجع مراجعه کن.

ممیزیِ تغییراتِ پیکربندی

Section titled “ممیزیِ تغییراتِ پیکربندی”

ردیابی کن که چه وقت فایل‌های تنظیمات یا skills در طولِ یک نشست تغییر می‌کنند. رویدادِ ConfigChange وقتی یک پردازشِ بیرونی یا ویرایشگر یک فایلِ پیکربندی را تغییر می‌دهد فعال می‌شود، بنابراین می‌توانی تغییرات را برای انطباق ثبت کنی یا تغییراتِ غیرمجاز را مسدود کنی.

این مثال هر تغییر را به یک لاگِ ممیزی پیوست می‌کند. این را به ~/.claude/settings.json اضافه کن:

{
"hooks": {
"ConfigChange": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
}
]
}
]
}
}

matcher بر اساسِ نوعِ پیکربندی فیلتر می‌کند: user_settings، project_settings، local_settings، policy_settings، یا skills. برای جلوگیری از اعمالِ یک تغییر، با کدِ ۲ خارج شو یا {"decision": "block"} بازگردان. برای اسکیمای کاملِ ورودی، مرجع ConfigChange را ببین.

بارگذاریِ مجددِ محیط هنگام تغییرِ دایرکتوری یا فایل‌ها

Section titled “بارگذاریِ مجددِ محیط هنگام تغییرِ دایرکتوری یا فایل‌ها”

برخی پروژه‌ها بسته به اینکه در کدام دایرکتوری هستی، متغیرهای محیطیِ متفاوتی تنظیم می‌کنند. ابزارهایی مثل direnv این کار را به‌طور خودکار در شِلِ تو انجام می‌دهند، اما ابزارِ Bashِ Claude خودش این تغییرات را برنمی‌دارد.

جفت‌کردنِ یک hookِ SessionStart با یک hookِ CwdChanged این را درست می‌کند. SessionStart متغیرها را برای دایرکتوری‌ای که در آن راه می‌اندازی بارگذاری می‌کند، و CwdChanged هر بار که Claude دایرکتوری را عوض می‌کند آن‌ها را دوباره بارگذاری می‌کند. هر دو در CLAUDE_ENV_FILE می‌نویسند که Claude Code آن را به‌عنوان مقدمه‌ی اسکریپت پیش از هر دستورِ Bash اجرا می‌کند. این را به ~/.claude/settings.json اضافه کن:

{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
],
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}

direnv allow را یک‌بار در هر دایرکتوری‌ای که .envrc دارد اجرا کن تا به direnv اجازه‌ی بارگذاریِ آن داده شود. اگر به‌جای direnv از devbox یا nix استفاده می‌کنی، همین الگو با devbox shellenv یا devbox global shellenv به‌جای direnv export bash کار می‌کند.

برای واکنش به فایل‌های مشخص به‌جای هر تغییرِ دایرکتوری، از FileChanged با یک matcher استفاده کن که نام‌فایل‌های موردِ پایش را فهرست می‌کند و با | از هم جدا شده‌اند. برای ساختنِ فهرستِ پایش، این مقدار به نام‌فایل‌های تحت‌اللفظی تقسیم می‌شود، نه اینکه به‌عنوان regex ارزیابی شود. FileChanged را ببین تا بفهمی همین مقدار چطور فیلتر می‌کند که هنگام تغییرِ یک فایل کدام گروه‌های hook اجرا شوند. این مثال .envrc و .env را در دایرکتوریِ کاری پایش می‌کند:

{
"hooks": {
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}

برای اسکیماهای ورودی، خروجیِ watchPaths، و جزئیاتِ CLAUDE_ENV_FILE، به مدخل‌های مرجعِ CwdChanged و FileChanged مراجعه کن.

تأییدِ خودکارِ پرامپت‌های مجوزِ مشخص

Section titled “تأییدِ خودکارِ پرامپت‌های مجوزِ مشخص”

دیالوگِ تأیید را برای فراخوانی‌های ابزاری که همیشه اجازه می‌دهی رد کن. این مثال ExitPlanMode را به‌طور خودکار تأیید می‌کند، ابزاری که Claude وقتی ارائه‌ی یک برنامه را تمام می‌کند و درخواستِ ادامه می‌دهد صدا می‌زند، تا هر بار که یک برنامه آماده است از تو پرسیده نشود.

برخلافِ مثال‌های کدِ خروجِ بالا، تأییدِ خودکار نیاز دارد که hookِ تو یک تصمیمِ JSON روی stdout بنویسد. یک hookِ PermissionRequest وقتی Claude Code قرار است یک دیالوگِ مجوز نشان دهد فعال می‌شود، و بازگرداندنِ "behavior": "allow" آن را به‌جای تو پاسخ می‌دهد.

matcher دامنه‌ی hook را فقط به ExitPlanMode محدود می‌کند، بنابراین هیچ پرامپتِ دیگری تحتِ‌تأثیر قرار نمی‌گیرد. این را به ~/.claude/settings.json اضافه کن:

{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}

وقتی hook تأیید می‌کند، Claude Code از حالتِ برنامه خارج می‌شود و هر حالتِ مجوزی را که پیش از ورود به حالتِ برنامه فعال بود بازمی‌گرداند. رونوشت در جایی که دیالوگ ظاهر می‌شد «Allowed by PermissionRequest hook» را نشان می‌دهد. مسیرِ hook همیشه گفت‌وگوی فعلی را نگه می‌دارد: نمی‌تواند کانتکست را پاک کند و یک نشستِ پیاده‌سازیِ تازه را آن‌طور که دیالوگ می‌تواند، شروع کند.

برای تنظیمِ یک حالتِ مجوزِ مشخص به‌جای آن، خروجیِ hookت می‌تواند شاملِ یک آرایه‌ی updatedPermissions با یک مدخلِ setMode باشد. مقدارِ mode هر حالتِ مجوزی مثل default، acceptEdits، یا bypassPermissions است، و destination: "session" آن را فقط برای نشستِ فعلی اعمال می‌کند.

برای تغییرِ نشست به acceptEdits، hookت این JSON را روی stdout می‌نویسد:

{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedPermissions": [
{ "type": "setMode", "mode": "acceptEdits", "destination": "session" }
]
}
}
}

matcher را تا حدِ ممکن باریک نگه دار. تطبیق روی .* یا گذاشتنِ matcherِ خالی هر پرامپتِ مجوزی را به‌طور خودکار تأیید می‌کند، از جمله نوشتنِ فایل‌ها و دستورهای شِل. برای مجموعه‌ی کاملِ فیلدهای تصمیم، به مرجع PermissionRequest مراجعه کن.

رویدادهای hook در نقاطِ مشخصی از چرخه‌ی عمرِ Claude Code فعال می‌شوند. وقتی یک رویداد فعال می‌شود، همه‌ی hookهای منطبق به‌صورت موازی اجرا می‌شوند، و دستورهای hookِ یکسان به‌طور خودکار حذفِ تکرار می‌شوند. جدولِ زیر هر رویداد و زمانِ فعال‌شدنش را نشان می‌دهد:

رویدادچه وقت فعال می‌شود
SessionStartوقتی یک نشست شروع می‌شود یا از سر گرفته می‌شود
Setupوقتی Claude Code را با --init-only، یا با --init یا --maintenance در حالتِ -p شروع می‌کنی. برای آماده‌سازیِ یک‌باره در CI یا اسکریپت‌ها
UserPromptSubmitوقتی یک پرامپت ارسال می‌کنی، پیش از اینکه Claude آن را پردازش کند
UserPromptExpansionوقتی یک دستورِ تایپ‌شده توسط کاربر به یک پرامپت بسط می‌یابد، پیش از رسیدن به Claude. می‌تواند بسط را مسدود کند
PreToolUseپیش از اجرای یک فراخوانیِ ابزار. می‌تواند آن را مسدود کند
PermissionRequestوقتی یک دیالوگِ مجوز ظاهر می‌شود
PermissionDeniedوقتی یک فراخوانیِ ابزار توسط طبقه‌بندِ حالتِ خودکار رد می‌شود. {retry: true} بازگردان تا به مدل بگویی می‌تواند فراخوانیِ ابزارِ ردشده را دوباره امتحان کند
PostToolUseپس از موفقیتِ یک فراخوانیِ ابزار
PostToolUseFailureپس از شکستِ یک فراخوانیِ ابزار
PostToolBatchپس از حل‌شدنِ یک دسته‌ی کاملِ فراخوانی‌های ابزارِ موازی، پیش از فراخوانیِ بعدیِ مدل
Notificationوقتی Claude Code یک اعلان می‌فرستد
MessageDisplayحین نمایشِ متنِ پیامِ دستیار
SubagentStartوقتی یک subagent ایجاد می‌شود
SubagentStopوقتی یک subagent تمام می‌کند
TaskCreatedوقتی یک کار از طریقِ TaskCreate در حالِ ایجاد است
TaskCompletedوقتی یک کار در حالِ علامت‌خوردن به‌عنوان تکمیل‌شده است
Stopوقتی Claude پاسخگویی را تمام می‌کند
StopFailureوقتی نوبت به‌خاطرِ یک خطای API پایان می‌یابد. خروجی و کدِ خروج نادیده گرفته می‌شوند
TeammateIdleوقتی یک هم‌تیمیِ تیمِ ایجنت در آستانه‌ی بی‌کار شدن است
InstructionsLoadedوقتی یک فایلِ CLAUDE.md یا .claude/rules/*.md در کانتکست بارگذاری می‌شود. در شروعِ نشست و وقتی فایل‌ها به‌صورت تنبل در طولِ یک نشست بارگذاری می‌شوند فعال می‌شود
ConfigChangeوقتی یک فایلِ پیکربندی در طولِ یک نشست تغییر می‌کند
CwdChangedوقتی دایرکتوریِ کاری تغییر می‌کند، مثلاً وقتی Claude یک دستورِ cd اجرا می‌کند. برای مدیریتِ واکنشیِ محیط با ابزارهایی مثل direnv مفید است
FileChangedوقتی یک فایلِ پایش‌شده روی دیسک تغییر می‌کند. فیلدِ matcher مشخص می‌کند کدام نام‌فایل‌ها پایش شوند
WorktreeCreateوقتی یک worktree از طریقِ --worktree یا isolation: "worktree" در حالِ ایجاد است. جایگزینِ رفتارِ پیش‌فرضِ git می‌شود
WorktreeRemoveوقتی یک worktree در حالِ حذف است، چه هنگام خروج از نشست و چه وقتی یک subagent تمام می‌کند
PreCompactپیش از فشرده‌سازیِ کانتکست
PostCompactپس از تکمیلِ فشرده‌سازیِ کانتکست
Elicitationوقتی یک سرورِ MCP حین یک فراخوانیِ ابزار درخواستِ ورودیِ کاربر می‌کند
ElicitationResultپس از اینکه یک کاربر به یک elicitationِ MCP پاسخ می‌دهد، پیش از بازفرستادنِ پاسخ به سرور
SessionEndوقتی یک نشست خاتمه می‌یابد

هر hook یک type دارد که نحوه‌ی اجرایش را تعیین می‌کند. بیشترِ hookها از "type": "command" استفاده می‌کنند که یک دستورِ شِل اجرا می‌کند. چهار نوعِ دیگر در دسترس‌اند:

  • "type": "http": داده‌ی رویداد را با POST به یک URL می‌فرستد. HTTP hooks را ببین.
  • "type": "mcp_tool": یک ابزار را روی یک سرورِ MCPِ از قبل متصل‌شده صدا می‌زند. MCP tool hooks را ببین.
  • "type": "prompt": ارزیابیِ تک‌نوبتیِ LLM. hooksِ مبتنی‌بر پرامپت را ببین.
  • "type": "agent": راستی‌آزماییِ چند‌نوبتی با دسترسی به ابزار. hooksِ ایجنت آزمایشی‌اند و ممکن است تغییر کنند. hooksِ مبتنی‌بر ایجنت را ببین.

وقتی چند hook با همان رویداد منطبق می‌شوند، دستورِ هر hook پیش از اینکه Claude Code نتایج را ادغام کند تا انتها اجرا می‌شود. بازگرداندنِ deny توسطِ یک hook اجرای hookهای هم‌رده را متوقف نمی‌کند. به denyِ یک hook برای سرکوبِ اثراتِ جانبیِ یک hookِ دیگر تکیه نکن.

پس از پایانِ همه‌ی hookهای منطبق، Claude Code خروجی‌هایشان را ترکیب می‌کند. برای تصمیم‌های مجوزِ PreToolUse، محدودکننده‌ترین پاسخ برنده می‌شود، به ترتیبِ deny، defer، ask، allow. متنِ حاصل از additionalContext از هر hook نگه داشته می‌شود و یکجا به Claude منتقل می‌شود.

مثالِ زیر دو hookِ PreToolUse را روی Bash ثبت می‌کند. اولی هر دستور را به یک فایلِ لاگ پیوست می‌کند و با ۰ خارج می‌شود. دومی اسکریپتی را اجرا می‌کند که وقتی دستور شاملِ rm -rf است با ۲ خارج می‌شود تا رد کند:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r .tool_input.command >> ~/.claude/bash.log"
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
]
}
]
}
}

وقتی Claude تلاش می‌کند rm -rf /tmp/build را اجرا کند، هر دو hook به‌صورت موازی اجرا می‌شوند. hookِ لاگ دستور را در ~/.claude/bash.log می‌نویسد و با ۰ خارج می‌شود، که هیچ تصمیمی گزارش نمی‌کند. hookِ گاردریل با ۲ خارج می‌شود، که فراخوانیِ ابزار را رد می‌کند. deny برنده می‌شود، بنابراین Claude Code دستور را مسدود می‌کند و stderrِ گاردریل را به Claude نشان می‌دهد. مدخلِ لاگ همچنان نوشته می‌شود چون hookِ لاگ از قبل اجرا شده بود.

خواندنِ ورودی و بازگرداندنِ خروجی

Section titled “خواندنِ ورودی و بازگرداندنِ خروجی”

hookها از طریقِ stdin، stdout، stderr، و کدهای خروج با Claude Code ارتباط می‌گیرند. وقتی یک رویداد فعال می‌شود، Claude Code داده‌ی مخصوصِ رویداد را به‌صورت JSON به stdinِ اسکریپتت می‌دهد. اسکریپتت آن داده را می‌خواند، کارش را می‌کند، و از طریقِ کدِ خروج به Claude Code می‌گوید بعد چه کند.

هر رویداد شاملِ فیلدهای مشترک مثل session_id و cwd است، اما هر نوعِ رویداد داده‌ی متفاوتی اضافه می‌کند. برای مثال، وقتی Claude یک دستورِ Bash اجرا می‌کند، یک hookِ PreToolUse چیزی شبیه این را روی stdin دریافت می‌کند:

{
"session_id": "abc123", // unique ID for this session
"cwd": "/Users/sarah/myproject", // working directory when the event fired
"hook_event_name": "PreToolUse", // which event triggered this hook
"tool_name": "Bash", // the tool Claude is about to use
"tool_input": { // the arguments Claude passed to the tool
"command": "npm test" // for Bash, this is the shell command
}
}

اسکریپتت می‌تواند آن JSON را تجزیه کند و روی هر یک از آن فیلدها عمل کند. hookهای UserPromptSubmit به‌جایش متنِ prompt را می‌گیرند، hookهای SessionStart مقدارِ source را می‌گیرند (startup، resume، clear، compact)، و الی آخر. برای فیلدهای مشترک به Common input fields در مرجع، و برای اسکیماهای مخصوصِ رویداد به بخشِ هر رویداد مراجعه کن.

اسکریپتت با نوشتن روی stdout یا stderr و خروج با یک کدِ مشخص به Claude Code می‌گوید بعد چه کند. برای مثال، یک hookِ PreToolUse که می‌خواهد یک دستور را مسدود کند:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr becomes Claude's feedback
exit 2 # exit 2 = block the action
fi
exit 0 # exit 0 = no decision; the normal permission flow applies

کدِ خروج تعیین می‌کند بعد چه می‌شود:

  • خروج ۰: hook هیچ اعتراضی گزارش نمی‌کند و کار به‌طور عادی پیش می‌رود. برای یک hookِ PreToolUse این فراخوانیِ ابزار را تأیید نمی‌کند: جریانِ مجوزِ عادی همچنان اعمال می‌شود. برای hookهای UserPromptSubmit، UserPromptExpansion، و SessionStart، هر چیزی که روی stdout بنویسی به کانتکستِ Claude افزوده می‌شود.
  • خروج ۲: کار مسدود می‌شود. دلیلی روی stderr بنویس، و Claude آن را به‌عنوان بازخورد دریافت می‌کند تا بتواند تنظیم کند. برخی رویدادها قابلِ مسدودشدن نیستند: برای SessionStart، Setup، Notification، و بقیه، خروج ۲ stderr را به کاربر نشان می‌دهد و اجرا ادامه می‌یابد. برای فهرستِ کامل به رفتارِ کدِ خروجِ ۲ به‌ازای هر رویداد مراجعه کن.
  • هر کدِ خروجِ دیگر: کار پیش می‌رود. رونوشت یک اعلانِ <hook name> hook error به‌دنبالِ اولین خطِ stderr نشان می‌دهد؛ کلِ stderr به لاگِ دیباگ می‌رود.

خروجیِ ساختاریافته‌ی JSON

Section titled “خروجیِ ساختاریافته‌ی JSON”

کدهای خروج فقط به تو اجازه می‌دهند مسدود کنی یا ساکت بمانی. برای کنترلِ بیشتر، با ۰ خارج شو و به‌جایش یک شیءِ JSON روی stdout چاپ کن.

برای مثال، یک hookِ PreToolUse می‌تواند یک فراخوانیِ ابزار را رد کند و به Claude بگوید چرا، یا آن را برای تأیید به کاربر ارجاع دهد:

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}

با "deny"، Claude Code فراخوانیِ ابزار را لغو می‌کند و permissionDecisionReason را به Claude بازمی‌گرداند. این مقادیرِ permissionDecision مخصوصِ PreToolUse هستند:

  • "allow": از پرامپتِ تعاملیِ مجوز رد شو. قواعدِ deny و ask، از جمله فهرست‌های denyِ مدیریت‌شده‌ی سازمانی، همچنان اعمال می‌شوند
  • "deny": فراخوانیِ ابزار را لغو کن و دلیل را به Claude بفرست
  • "ask": پرامپتِ مجوز را به‌طور عادی به کاربر نشان بده

یک مقدارِ چهارم، "defer"، در حالتِ غیرتعاملی با پرچمِ -p در دسترس است. پردازش را با حفظِ فراخوانیِ ابزار خارج می‌کند تا یک رَپِر Agent SDK بتواند ورودی جمع کند و از سر بگیرد. به یک فراخوانیِ ابزار را برای بعد به‌تعویق بینداز در مرجع مراجعه کن.

بازگرداندنِ "allow" از پرامپتِ تعاملی رد می‌شود اما قواعدِ مجوز را نادیده نمی‌گیرد. اگر یک قاعده‌ی deny با فراخوانیِ ابزار منطبق شود، فراخوانی مسدود می‌شود حتی وقتی hookت "allow" بازمی‌گرداند. اگر یک قاعده‌ی ask منطبق شود، همچنان از کاربر پرسیده می‌شود. این یعنی قواعدِ deny از هر دامنه‌ی تنظیمات، از جمله تنظیماتِ مدیریت‌شده، همیشه بر تأییدهای hook اولویت دارند.

رویدادهای دیگر از الگوهای تصمیمِ متفاوتی استفاده می‌کنند. برای مثال، hookهای PostToolUse و Stop از یک فیلدِ سطح‌بالای decision: "block" استفاده می‌کنند، در حالی که PermissionRequest از hookSpecificOutput.decision.behavior استفاده می‌کند. برای تفکیکِ کامل به‌ازای هر رویداد، به جدولِ خلاصه در مرجع مراجعه کن.

برای hookهای UserPromptSubmit، به‌جایش از additionalContext برای تزریقِ متن به کانتکستِ Claude استفاده کن. hookهای مبتنی‌بر پرامپت (type: "prompt") خروجی را متفاوت مدیریت می‌کنند: hooksِ مبتنی‌بر پرامپت را ببین.

فیلتر کردنِ hookها با matcherها

Section titled “فیلتر کردنِ hookها با matcherها”

بدونِ matcher، یک hook روی هر رخدادِ رویدادش فعال می‌شود. matcherها به تو امکان می‌دهند این را محدودتر کنی. برای مثال، اگر می‌خواهی یک قالب‌بند را فقط پس از ویرایشِ فایل (نه پس از هر فراخوانیِ ابزار) اجرا کنی، یک matcher به hookِ PostToolUseات اضافه کن:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "prettier --write ..." }
]
}
]
}
}

matcherِ "Edit|Write" فقط وقتی Claude از ابزارِ Edit یا Write استفاده می‌کند فعال می‌شود، نه وقتی از Bash، Read، یا هر ابزارِ دیگر استفاده می‌کند. برای نحوه‌ی ارزیابیِ نام‌های ساده و عباراتِ منظم، Matcher patterns را ببین.

هر نوعِ رویداد روی یک فیلدِ مشخص تطبیق می‌دهد:

رویدادmatcher چه چیزی را فیلتر می‌کندمقادیرِ نمونه‌ی matcher
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDeniedنامِ ابزارBash, Edit|Write, mcp__.*
SessionStartچطور نشست شروع شدstartup, resume, clear, compact
Setupکدام پرچمِ CLI راه‌اندازی را فعال کردinit, maintenance
SessionEndچرا نشست پایان یافتclear, resume, logout, prompt_input_exit, bypass_permissions_disabled, other
Notificationنوعِ اعلانpermission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_complete, elicitation_response
SubagentStartنوعِ ایجنتgeneral-purpose, Explore, Plan, یا نام‌های ایجنتِ سفارشی
PreCompact, PostCompactچه چیزی فشرده‌سازی را فعال کردmanual, auto
SubagentStopنوعِ ایجنتهمان مقادیرِ SubagentStart
ConfigChangeمنبعِ پیکربندیuser_settings, project_settings, local_settings, policy_settings, skills
StopFailureنوعِ خطاrate_limit, overloaded, authentication_failed, oauth_org_not_allowed, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, unknown
InstructionsLoadedدلیلِ بارگذاریsession_start, nested_traversal, path_glob_match, include, compact
Elicitationنامِ سرورِ MCPنام‌های سرورِ MCPِ پیکربندی‌شده‌ات
ElicitationResultنامِ سرورِ MCPهمان مقادیرِ Elicitation
FileChangedنام‌فایل‌های تحت‌اللفظی برای پایش (نگاه کن به FileChanged).envrc|.env
UserPromptExpansionنامِ دستورنام‌های skill یا دستورِ تو
UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, CwdChanged, MessageDisplayپشتیبانی از matcher نداردهمیشه روی هر رخداد فعال می‌شود

چند مثالِ بیشتر که matcherها را روی انواعِ مختلفِ رویداد نشان می‌دهند:

فقط فراخوانی‌های ابزارِ Bash را تطبیق بده و هر دستور را در یک فایل لاگ کن. رویدادِ PostToolUse پس از تکمیلِ دستور فعال می‌شود، بنابراین tool_input.command شاملِ آن چیزی است که اجرا شد. hook داده‌ی رویداد را به‌صورت JSON روی stdin دریافت می‌کند، و jq -r '.tool_input.command' فقط رشته‌ی دستور را استخراج می‌کند، که >> آن را به فایلِ لاگ پیوست می‌کند:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}
]
}
]
}
}

برای نحوِ کاملِ matcher، به مرجع Hooks مراجعه کن.

فیلتر کردن بر اساسِ نامِ ابزار و آرگومان‌ها با فیلدِ if

Section titled “فیلتر کردن بر اساسِ نامِ ابزار و آرگومان‌ها با فیلدِ if”

فیلدِ if از نحوِ قاعده‌ی مجوز برای فیلترکردنِ hookها بر اساسِ نامِ ابزار و آرگومان‌ها با هم استفاده می‌کند، بنابراین پردازشِ hook فقط وقتی فراخوانیِ ابزار منطبق شود ایجاد می‌شود. این فراتر از matcher می‌رود، که در سطحِ گروه فقط بر اساسِ نامِ ابزار فیلتر می‌کند.

برای مثال، برای اجرای یک hook فقط وقتی Claude از دستورهای git استفاده می‌کند نه همه‌ی دستورهای Bash:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}

اینکه دستورِ hookت اجرا می‌شود یا نه به شکلِ الگوی ifات و دستورِ Bashی که Claude صدا می‌زند بستگی دارد:

الگوی ifدستورِ Bashhook اجرا می‌شود؟چرا
Bash(git *)git pushبلهنامِ دستور منطبق می‌شود
Bash(git *)npm test && git pushبلههر زیردستور بررسی می‌شود؛ git push منطبق می‌شود
Bash(git *)echo $(git log)بلهدستورهای داخلِ $() و بک‌تیک‌ها بررسی می‌شوند؛ git log منطبق می‌شود
Bash(git *)echo $(date)نههیچ زیردستوری با git * منطبق نمی‌شود
Bash(git push *)echo $(date)بلهالگوهایی که بیش از نامِ دستور را مشخص می‌کنند، در هر صورت hook را روی $()، بک‌تیک‌ها، یا $VAR اجرا می‌کنند

این فیلتر هنگامی که دستورِ Bash قابلِ تجزیه نباشد نیز fail open می‌شود، یعنی hookت را صرف‌نظر از الگو اجرا می‌کند. چون فیلتر best-effort است، برای اعمالِ یک allow یا denyِ قطعی به‌جای hook از سیستمِ مجوز استفاده کن.

فیلدِ if همان الگوهای قواعدِ مجوز را می‌پذیرد: "Bash(git *)"، "Edit(*.ts)"، و الی آخر. برای تطبیق با چند نامِ ابزار، از هندلرهای جداگانه هرکدام با مقدارِ if خودشان استفاده کن، یا در سطحِ matcher تطبیق بده که در آنجا تناوبِ pipe پشتیبانی می‌شود.

if فقط روی رویدادهای ابزار کار می‌کند: PreToolUse، PostToolUse، PostToolUseFailure، PermissionRequest، و PermissionDenied. افزودنِ آن به هر رویدادِ دیگر مانعِ اجرای hook می‌شود.

جایی که hook را اضافه می‌کنی دامنه‌اش را تعیین می‌کند:

محلدامنهقابلِ هم‌رسانی
~/.claude/settings.jsonهمه‌ی پروژه‌هایتنه، محلیِ ماشینِ توست
.claude/settings.jsonیک پروژه‌ی واحدبله، می‌تواند به مخزن کامیت شود
.claude/settings.local.jsonیک پروژه‌ی واحدنه، وقتی Claude Code می‌سازدش gitignore می‌شود
تنظیماتِ خط‌مشیِ مدیریت‌شدهسراسرِ سازمانبله، تحتِ کنترلِ ادمین
پلاگین hooks/hooks.jsonوقتی پلاگین فعال استبله، همراهِ پلاگین بسته‌بندی می‌شود
frontmatterِ Skill یا agentتا وقتی skill یا agent فعال استبله، در فایلِ کامپوننت تعریف می‌شود

/hooks را در Claude Code اجرا کن تا همه‌ی hookهای پیکربندی‌شده را گروه‌بندی‌شده بر اساسِ رویداد مرور کنی. برای غیرفعال‌کردنِ hookها، "disableAllHooks": true را در فایلِ تنظیماتت تنظیم کن. hookهای پیکربندی‌شده در تنظیماتِ مدیریت‌شده همچنان اجرا می‌شوند مگر اینکه disableAllHooks آنجا هم تنظیم شده باشد.

اگر فایل‌های تنظیمات را مستقیماً حین اجرای Claude Code ویرایش کنی، پایشگرِ فایل معمولاً تغییراتِ hook را به‌طور خودکار برمی‌دارد.

برای تصمیم‌هایی که به‌جای قواعدِ قطعی نیاز به قضاوت دارند، از hookهای type: "prompt" استفاده کن. به‌جای اجرای یک دستورِ شِل، Claude Code پرامپتت و داده‌ی ورودیِ hook را به یک مدلِ Claude (به‌طور پیش‌فرض Haiku) می‌فرستد تا تصمیم بگیرد. اگر به توانمندیِ بیشتری نیاز داری می‌توانی با فیلدِ model مدلِ دیگری را مشخص کنی.

تنها وظیفه‌ی مدل بازگرداندنِ یک تصمیمِ بله/خیر به‌صورت JSON است:

  • "ok": true: کار پیش می‌رود
  • "ok": false: آنچه اتفاق می‌افتد به رویداد بستگی دارد:
    • Stop و SubagentStop: مقدارِ reason به Claude بازگردانده می‌شود تا به کار ادامه دهد
    • PreToolUse: فراخوانیِ ابزار رد می‌شود و مقدارِ reason به‌عنوان خطای ابزار به Claude بازگردانده می‌شود، تا بتواند تنظیم کند و ادامه دهد
    • PostToolUse، PostToolBatch، UserPromptSubmit، و UserPromptExpansion: نوبت پایان می‌یابد و مقدارِ reason به‌عنوان یک خطِ هشدار در چت ظاهر می‌شود

این مثال از یک hookِ Stop استفاده می‌کند تا از مدل بپرسد آیا همه‌ی کارهای درخواست‌شده کامل‌اند. اگر مدل "ok": false بازگرداند، Claude به کار ادامه می‌دهد و مقدارِ reason را به‌عنوان دستورِ بعدی‌اش به کار می‌گیرد:

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}

برای گزینه‌های کاملِ پیکربندی، به hooksِ مبتنی‌بر پرامپت در مرجع مراجعه کن.

وقتی راستی‌آزمایی نیازمندِ بازرسیِ فایل‌ها یا اجرای دستورهاست، از hookهای type: "agent" استفاده کن. برخلافِ hookهای پرامپت که یک فراخوانیِ تکیِ LLM انجام می‌دهند، hookهای ایجنت یک subagent ایجاد می‌کنند که می‌تواند فایل‌ها را بخواند، کد را جستجو کند، و از ابزارهای دیگر برای راستی‌آزماییِ شرط‌ها پیش از بازگرداندنِ یک تصمیم استفاده کند.

hookهای ایجنت از همان قالبِ پاسخِ "ok" / "reason"ِ hookهای پرامپت استفاده می‌کنند، اما با یک تایم‌اوتِ پیش‌فرضِ طولانی‌ترِ ۶۰ ثانیه و تا ۵۰ نوبتِ استفاده از ابزار.

این مثال راستی‌آزمایی می‌کند که تست‌ها پیش از اجازه‌ی توقفِ Claude قبول می‌شوند:

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}

از hookهای پرامپت وقتی استفاده کن که داده‌ی ورودیِ hook به‌تنهایی برای تصمیم‌گیری کافی است. از hookهای ایجنت وقتی استفاده کن که نیاز داری چیزی را در برابرِ وضعیتِ واقعیِ کدبیس راستی‌آزمایی کنی.

برای گزینه‌های کاملِ پیکربندی، به hooksِ مبتنی‌بر ایجنت در مرجع مراجعه کن.

از hookهای type: "http" برای فرستادنِ داده‌ی رویداد با POST به یک نقطه‌پایانِ HTTP به‌جای اجرای یک دستورِ شِل استفاده کن. نقطه‌پایان همان JSONی را که یک hookِ دستور روی stdin دریافت می‌کرد دریافت می‌کند، و نتایج را از طریقِ بدنه‌ی پاسخِ HTTP با همان قالبِ JSON بازمی‌گرداند.

HTTP hooks وقتی مفیدند که بخواهی یک وب‌سرور، تابعِ ابری، یا سرویسِ بیرونی منطقِ hook را مدیریت کند: برای مثال، یک سرویسِ ممیزیِ مشترک که رویدادهای استفاده از ابزار را در سراسرِ یک تیم لاگ می‌کند.

این مثال هر استفاده از ابزار را به یک سرویسِ لاگِ محلی POST می‌کند:

{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}

نقطه‌پایان باید یک بدنه‌ی پاسخِ JSON با همان قالبِ خروجیِ hookهای دستور بازگرداند. برای مسدودکردنِ یک فراخوانیِ ابزار، یک پاسخِ 2xx با فیلدهای مناسبِ hookSpecificOutput بازگردان. کدهای وضعیتِ HTTP به‌تنهایی نمی‌توانند کارها را مسدود کنند.

مقادیرِ هدر از درون‌یابیِ متغیرهای محیطی با نحوِ $VAR_NAME یا ${VAR_NAME} پشتیبانی می‌کنند. فقط متغیرهای فهرست‌شده در آرایه‌ی allowedEnvVars resolve می‌شوند؛ همه‌ی ارجاع‌های $VARِ دیگر خالی می‌مانند.

برای گزینه‌های کاملِ پیکربندی و مدیریتِ پاسخ، به HTTP hooks در مرجع مراجعه کن.

محدودیت‌ها و عیب‌یابی

Section titled “محدودیت‌ها و عیب‌یابی”
  • hookهای دستور فقط از طریقِ stdout، stderr، و کدهای خروج ارتباط می‌گیرند. آن‌ها نمی‌توانند دستورهای / یا فراخوانی‌های ابزار را فعال کنند. متنِ بازگردانده‌شده از طریقِ additionalContext به‌عنوان یک یادآورِ سیستمی تزریق می‌شود که Claude آن را به‌عنوان متنِ ساده می‌خواند. HTTP hooks به‌جای آن از طریقِ بدنه‌ی پاسخ ارتباط می‌گیرند.
  • تایم‌اوتِ hookها بر اساسِ نوع متفاوت است. با فیلدِ timeout بر حسبِ ثانیه به‌ازای هر hook بازنویسی کن.
    • command، http، mcp_tool: ۱۰ دقیقه. UserPromptSubmit این‌ها را به ۳۰ ثانیه و MessageDisplay آن‌ها را به ۱۰ ثانیه کاهش می‌دهد.
    • prompt: ۳۰ ثانیه.
    • agent: ۶۰ ثانیه.
  • hookهای PostToolUse نمی‌توانند کارها را برگردانند چون ابزار از قبل اجرا شده است.
  • hookهای PermissionRequest در حالتِ غیرتعاملی (-p) فعال نمی‌شوند. برای تصمیم‌های مجوزِ خودکار از hookهای PreToolUse استفاده کن.
  • hookهای Stop هر وقت Claude پاسخگویی را تمام می‌کند فعال می‌شوند، نه فقط در تکمیلِ کار. آن‌ها روی وقفه‌های کاربر فعال نمی‌شوند. خطاهای API به‌جای آن StopFailure را فعال می‌کنند.
  • وقتی چند hookِ PreToolUse مقدارِ updatedInput را برای بازنویسیِ آرگومان‌های یک ابزار بازمی‌گردانند، آخرین hookی که تمام شود برنده می‌شود. چون hookها به‌صورت موازی اجرا می‌شوند، ترتیب غیرقطعی است. از داشتنِ بیش از یک hook که ورودیِ همان ابزار را تغییر دهد بپرهیز.

hookهای PreToolUse پیش از هر بررسیِ حالتِ مجوز فعال می‌شوند. یک hook که permissionDecision: "deny" بازمی‌گرداند ابزار را حتی در حالتِ bypassPermissions یا با --dangerously-skip-permissions مسدود می‌کند. این به تو امکان می‌دهد خط‌مشی‌ای را اعمال کنی که کاربران نمی‌توانند با تغییرِ حالتِ مجوزشان دورش بزنند.

عکسش درست نیست: یک hook که "allow" بازمی‌گرداند قواعدِ deny از تنظیمات را دور نمی‌زند. hookها می‌توانند محدودیت‌ها را سفت‌تر کنند اما نمی‌توانند فراتر از آنچه قواعدِ مجوز اجازه می‌دهند شل‌شان کنند.

hook پیکربندی شده اما هرگز اجرا نمی‌شود.

  • /hooks را اجرا کن و تأیید کن hook زیرِ رویدادِ درست ظاهر می‌شود
  • بررسی کن که الگوی matcher دقیقاً با نامِ ابزار منطبق است (matcherها به بزرگی و کوچکیِ حروف حساس‌اند)
  • راستی‌آزمایی کن که نوعِ رویدادِ درست را فعال می‌کنی (مثلاً PreToolUse پیش از اجرای ابزار و PostToolUse پس از آن فعال می‌شود)
  • اگر از hookهای PermissionRequest در حالتِ غیرتعاملی (-p) استفاده می‌کنی، به‌جایش به PreToolUse تغییر بده

پیامی مثل «PreToolUse hook error: …» در رونوشت می‌بینی.

  • اسکریپتت به‌طور غیرمنتظره با یک کدِ غیرصفر خارج شده است. آن را دستی با لوله‌کردنِ JSONِ نمونه آزمایش کن:
    Terminal window
    echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
    echo $? # Check the exit code
  • اگر «command not found» می‌بینی، از مسیرهای مطلق یا ${CLAUDE_PROJECT_DIR} برای ارجاع به اسکریپت‌ها استفاده کن. برای پرهیز کامل از quotingِ شِل، "args": [] را اضافه کن تا به فرمِ exec تغییر دهی، که اسکریپت را مستقیماً بدونِ شِل ایجاد می‌کند
  • اگر «jq: command not found» می‌بینی، jq را نصب کن یا برای تجزیه‌ی JSON از Python/Node.js استفاده کن
  • اگر اسکریپت اصلاً اجرا نمی‌شود، آن را اجراشدنی کن: chmod +x ./my-hook.sh

/hooks هیچ hookِ پیکربندی‌شده‌ای نشان نمی‌دهد

Section titled “/hooks هیچ hookِ پیکربندی‌شده‌ای نشان نمی‌دهد”

یک فایلِ تنظیمات را ویرایش کردی اما hookها در منو ظاهر نمی‌شوند.

  • ویرایش‌های فایل معمولاً به‌طور خودکار برداشته می‌شوند. اگر پس از چند ثانیه ظاهر نشدند، ممکن است پایشگرِ فایل تغییر را از دست داده باشد: نشستت را برای واداشتن به بارگذاریِ مجدد ری‌استارت کن.
  • راستی‌آزمایی کن که JSONت معتبر است (کاما و کامنتِ انتهایی مجاز نیستند)
  • تأیید کن که فایلِ تنظیمات در محلِ درست است: .claude/settings.json برای hookهای پروژه، ~/.claude/settings.json برای hookهای سراسری

hookِ Stop به سقفِ مسدودسازی می‌خورد

Section titled “hookِ Stop به سقفِ مسدودسازی می‌خورد”

Claude به‌جای توقف به کار ادامه می‌دهد، سپس نوبت را با هشداری پایان می‌دهد که hookِ Stop بیش از حد پشتِ سرِ هم مسدود کرده است.

Claude Code یک hookِ Stop را پس از ۸ بار مسدودسازیِ متوالی بدونِ پیشرفت کنار می‌گذارد. اسکریپتِ hookت باید بررسی کند که آیا از قبل یک ادامه را فعال کرده است. فیلدِ stop_hook_active را از ورودیِ JSON تجزیه کن و اگر true بود زود خارج شو:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic

اگر hookت به‌طور مشروع برای هم‌گرایی به بیش از هشت تکرار نیاز دارد، سقف را با CLAUDE_CODE_STOP_HOOK_BLOCK_CAP بالا ببر.

اعتبارسنجیِ JSON شکست خورد

Section titled “اعتبارسنجیِ JSON شکست خورد”

Claude Code یک خطای تجزیه‌ی JSON نشان می‌دهد، حتی با وجودِ اینکه اسکریپتِ hookت JSONِ معتبر خروجی می‌دهد.

وقتی Claude Code یک hookِ دستورِ فرمِ شِل (بدونِ args) را اجرا می‌کند، روی macOS و Linux به‌طور پیش‌فرض sh -c و روی Windows ‏Git Bash را ایجاد می‌کند. این شِل غیرتعاملی است، اما Git Bash و برخی پیکربندی‌ها (مثلِ BASH_ENV که به ~/.bashrc اشاره می‌کند) همچنان پروفایلت را source می‌کنند. اگر آن پروفایل شاملِ دستورهای echoِ بی‌قید‌وشرط باشد، خروجی به ابتدای JSONِ hookت اضافه می‌شود:

Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}

Claude Code تلاش می‌کند این را به‌عنوان JSON تجزیه کند و شکست می‌خورد. برای رفعِ این، دستورهای echo را در پروفایلِ شِلت طوری بپیچان که فقط در شِل‌های تعاملی اجرا شوند:

Terminal window
# In ~/.zshrc or ~/.bashrc
if [[ $- == *i* ]]; then
echo "Shell ready"
fi

متغیرِ $- شاملِ پرچم‌های شِل است، و i یعنی تعاملی. hookها در شِل‌های غیرتعاملی اجرا می‌شوند، بنابراین echo رد می‌شود.

نمای رونوشت، که با Ctrl+O تغییرِ وضعیت می‌دهد، یک خلاصه‌ی یک‌خطی برای هر hookی که فعال شد نشان می‌دهد: موفقیت ساکت است، خطاهای مسدودکننده stderr را نشان می‌دهند، و خطاهای غیرمسدودکننده یک اعلانِ <hook name> hook error به‌دنبالِ اولین خطِ stderr نشان می‌دهند.

برای جزئیاتِ کاملِ اجرا شاملِ اینکه کدام hookها منطبق شدند، کدهای خروجشان، stdout، و stderr، لاگِ دیباگ را بخوان. Claude Code را با claude --debug-file /tmp/claude.log شروع کن تا در یک مسیرِ مشخص بنویسد، سپس tail -f /tmp/claude.log را در ترمینالِ دیگری اجرا کن. اگر بدونِ آن پرچم شروع کردی، /debug را وسطِ نشست اجرا کن تا لاگ‌گیری را فعال کنی و مسیرِ لاگ را بیابی.