Follow Discord
Sweep 03 Nov 2026 · 22:27Z Build v2.1.318 536 read Stable v2.1.311 Latest v2.1.318 Next v2.1.318 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Hooks reference changedhooks

Nearest release: v2.1.314, published 4 hours after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 29 Oct 2026 19:44 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 30 Oct 2026 18:07 UTC.

Upstream edited
Recorded here
Lines+414added
Lines−414removed
From line 64 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits127to this page, all time

The whole hunk

from line 64, old and new numbered
/
lines

This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 64
6464 
6565To see how the event, the matcher, and the handler fit together, consider this `PreToolUse` hook that blocks destructive shell commands.
6666 
67**macOS/Linux**
68 
69The `matcher` narrows to Bash tool calls and the `if` condition narrows further to Bash subcommands matching `rm *`, so `block-rm.sh` only spawns when both filters match:
70 
71```json theme={null}
72{
73 "hooks": {
74 "PreToolUse": [
75 {
76 "matcher": "Bash",
77 "hooks": [
78 {
79 "type": "command",
80 "if": "Bash(rm *)",
81 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
82 "args": []
83 }
84 ]
85 }
86 ]
87 }
88}
89```
90 
91The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf`. Save it to `.claude/hooks/block-rm.sh` in your project and make it executable with `chmod +x .claude/hooks/block-rm.sh` so Claude Code can run it:
92 
93```bash theme={null}
94#!/bin/bash
95# .claude/hooks/block-rm.sh
96COMMAND=$(jq -r '.tool_input.command')
97 
98if echo "$COMMAND" | grep -q 'rm -rf'; then
99 jq -n '{
100 hookSpecificOutput: {
101 hookEventName: "PreToolUse",
102 permissionDecision: "deny",
103 permissionDecisionReason: "Destructive command blocked by hook"
104 }
105 }'
106else
107 exit 0 # no decision; normal permission flow applies
108fi
109```
110 
111This script, like the other Bash examples on this page that parse JSON input, uses `jq`, so install `jq` and make sure it is on your `PATH` before trying them.
112 
113**Windows (PowerShell)**
114 
115The matcher `Bash|PowerShell` covers the [PowerShell tool](#powershell) as well as Bash. A single `if` rule matches only one tool's calls, so each tool gets its own handler: the first narrows to Bash subcommands matching `rm *`, the second to PowerShell commands matching `Remove-Item *`. Both run the same script through `powershell.exe`:
116 
117```json theme={null}
118{
119 "hooks": {
120 "PreToolUse": [
121 {
122 "matcher": "Bash|PowerShell",
123 "hooks": [
124 {
125 "type": "command",
126 "if": "Bash(rm *)",
127 "command": "powershell.exe",
128 "args": [
129 "-NoProfile",
130 "-ExecutionPolicy",
131 "Bypass",
132 "-File",
133 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
134 ]
135 },
136 {
137 "type": "command",
138 "if": "PowerShell(Remove-Item *)",
139 "command": "powershell.exe",
140 "args": [
141 "-NoProfile",
142 "-ExecutionPolicy",
143 "Bypass",
144 "-File",
145 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
146 ]
147 }
148 ]
149 }
150 ]
151 }
152}
153```
154 
155The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
156 
157The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf` or `Remove-Item` followed by `-Recurse`. Save it to `.claude/hooks/block-rm.ps1` in your project:
158 
159```powershell theme={null}
160# .claude/hooks/block-rm.ps1
161$callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
162$command = $callInput.tool_input.command
163 
164if ($command -match 'rm -rf|Remove-Item.*-Recurse') {
165 @{
166 hookSpecificOutput = @{
167 hookEventName = "PreToolUse"
168 permissionDecision = "deny"
169 permissionDecisionReason = "Destructive command blocked by hook"
170 }
171 } | ConvertTo-Json
172} else {
173 exit 0 # no decision; normal permission flow applies
174}
175```
67> [tab] macOS/Linux
68>
69> The `matcher` narrows to Bash tool calls and the `if` condition narrows further to Bash subcommands matching `rm *`, so `block-rm.sh` only spawns when both filters match:
70>
71> ```json theme={null}
72> {
73> "hooks": {
74> "PreToolUse": [
75> {
76> "matcher": "Bash",
77> "hooks": [
78> {
79> "type": "command",
80> "if": "Bash(rm *)",
81> "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
82> "args": []
83> }
84> ]
85> }
86> ]
87> }
88> }
89> ```
90>
91> The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf`. Save it to `.claude/hooks/block-rm.sh` in your project and make it executable with `chmod +x .claude/hooks/block-rm.sh` so Claude Code can run it:
92>
93> ```bash theme={null}
94> #!/bin/bash
95> # .claude/hooks/block-rm.sh
96> COMMAND=$(jq -r '.tool_input.command')
97>
98> if echo "$COMMAND" | grep -q 'rm -rf'; then
99> jq -n '{
100> hookSpecificOutput: {
101> hookEventName: "PreToolUse",
102> permissionDecision: "deny",
103> permissionDecisionReason: "Destructive command blocked by hook"
104> }
105> }'
106> else
107> exit 0 # no decision; normal permission flow applies
108> fi
109> ```
110>
111> This script, like the other Bash examples on this page that parse JSON input, uses `jq`, so install `jq` and make sure it is on your `PATH` before trying them.
112 
113> [tab] Windows (PowerShell)
114>
115> The matcher `Bash|PowerShell` covers the [PowerShell tool](#powershell) as well as Bash. A single `if` rule matches only one tool's calls, so each tool gets its own handler: the first narrows to Bash subcommands matching `rm *`, the second to PowerShell commands matching `Remove-Item *`. Both run the same script through `powershell.exe`:
116>
117> ```json theme={null}
118> {
119> "hooks": {
120> "PreToolUse": [
121> {
122> "matcher": "Bash|PowerShell",
123> "hooks": [
124> {
125> "type": "command",
126> "if": "Bash(rm *)",
127> "command": "powershell.exe",
128> "args": [
129> "-NoProfile",
130> "-ExecutionPolicy",
131> "Bypass",
132> "-File",
133> "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
134> ]
135> },
136> {
137> "type": "command",
138> "if": "PowerShell(Remove-Item *)",
139> "command": "powershell.exe",
140> "args": [
141> "-NoProfile",
142> "-ExecutionPolicy",
143> "Bypass",
144> "-File",
145> "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
146> ]
147> }
148> ]
149> }
150> ]
151> }
152> }
153> ```
154>
155> The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
156>
157> The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf` or `Remove-Item` followed by `-Recurse`. Save it to `.claude/hooks/block-rm.ps1` in your project:
158>
159> ```powershell theme={null}
160> # .claude/hooks/block-rm.ps1
161> $callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
162> $command = $callInput.tool_input.command
163>
164> if ($command -match 'rm -rf|Remove-Item.*-Recurse') {
165> @{
166> hookSpecificOutput = @{
167> hookEventName = "PreToolUse"
168> permissionDecision = "deny"
169> permissionDecisionReason = "Destructive command blocked by hook"
170> }
171> } | ConvertTo-Json
172> } else {
173> exit 0 # no decision; normal permission flow applies
174> }
175> ```
176176 
177177Now suppose Claude Code decides to run `Bash "rm -rf /tmp/build"` against the macOS/Linux config. Here's what happens:
178178 
from line 597
597597 
598598Prefer [exec form](#exec-form-and-shell-form) for any hook that references a path placeholder. In shell form, wrap each placeholder in double quotes.
599599 
600**Project scripts**
601 
602This example uses `${CLAUDE_PROJECT_DIR}` to run a style checker from the project's `.claude/hooks/` directory after any `Write` or `Edit` tool call:
603 
604```json theme={null}
605{
606 "hooks": {
607 "PostToolUse": [
608 {
609 "matcher": "Write|Edit",
610 "hooks": [
611 {
612 "type": "command",
613 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
614 "args": []
615 }
616 ]
617 }
618 ]
619 }
620}
621```
622 
623**Plugin scripts**
624 
625Define plugin hooks in `hooks/hooks.json` with an optional top-level `description` field. When a plugin is enabled, its hooks merge with your user and project hooks.
626 
627This example runs a formatting script bundled with the plugin:
628 
629```json theme={null}
630{
631 "description": "Automatic code formatting",
632 "hooks": {
633 "PostToolUse": [
634 {
635 "matcher": "Write|Edit",
636 "hooks": [
637 {
638 "type": "command",
639 "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
640 "args": [],
641 "timeout": 30
642 }
643 ]
644 }
645 ]
646 }
647}
648```
649 
650See the [plugin components reference](https://code.claude.com/docs/en/plugins/components.md#hooks) for details on creating plugin hooks.
600> [tab] Project scripts
601>
602> This example uses `${CLAUDE_PROJECT_DIR}` to run a style checker from the project's `.claude/hooks/` directory after any `Write` or `Edit` tool call:
603>
604> ```json theme={null}
605> {
606> "hooks": {
607> "PostToolUse": [
608> {
609> "matcher": "Write|Edit",
610> "hooks": [
611> {
612> "type": "command",
613> "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
614> "args": []
615> }
616> ]
617> }
618> ]
619> }
620> }
621> ```
622 
623> [tab] Plugin scripts
624>
625> Define plugin hooks in `hooks/hooks.json` with an optional top-level `description` field. When a plugin is enabled, its hooks merge with your user and project hooks.
626>
627> This example runs a formatting script bundled with the plugin:
628>
629> ```json theme={null}
630> {
631> "description": "Automatic code formatting",
632> "hooks": {
633> "PostToolUse": [
634> {
635> "matcher": "Write|Edit",
636> "hooks": [
637> {
638> "type": "command",
639> "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
640> "args": [],
641> "timeout": 30
642> }
643> ]
644> }
645> ]
646> }
647> }
648> ```
649>
650> See the [plugin components reference](https://code.claude.com/docs/en/plugins/components.md#hooks) for details on creating plugin hooks.
651651 
652652### Hooks in skills and agents
653653 
from line 1176
11761176 
11771177Here are examples of each pattern in action:
11781178 
1179**Top-level decision**
1180 
1181The only value for `decision` is `"block"`. To allow the action to proceed, omit `decision` from your JSON, or exit 0 without any JSON at all:
1182 
1183```json theme={null}
1184{
1185 "decision": "block",
1186 "reason": "Test suite must pass before proceeding"
1187}
1188```
1189 
1190**PreToolUse**
1191 
1192Uses `hookSpecificOutput` for richer control: allow, deny, or escalate to the user. You can also modify tool input before it runs or inject additional context for Claude. See [PreToolUse decision control](#pretooluse-decision-control) for the full set of options.
1193 
1194```json theme={null}
1195{
1196 "hookSpecificOutput": {
1197 "hookEventName": "PreToolUse",
1198 "permissionDecision": "deny",
1199 "permissionDecisionReason": "Database writes are not allowed"
1200 }
1201}
1202```
1203 
1204**PermissionRequest**
1205 
1206Uses `hookSpecificOutput` to allow or deny a permission request on behalf of the user. When allowing, you can also modify the tool's input or apply permission rules so the user isn't prompted again. See [PermissionRequest decision control](#permissionrequest-decision-control) for the full set of options.
1207 
1208```json theme={null}
1209{
1210 "hookSpecificOutput": {
1211 "hookEventName": "PermissionRequest",
1212 "decision": {
1213 "behavior": "allow",
1214 "updatedInput": {
1215 "command": "npm run lint"
1216 }
1217 }
1218 }
1219}
1220```
1179> [tab] Top-level decision
1180>
1181> The only value for `decision` is `"block"`. To allow the action to proceed, omit `decision` from your JSON, or exit 0 without any JSON at all:
1182>
1183> ```json theme={null}
1184> {
1185> "decision": "block",
1186> "reason": "Test suite must pass before proceeding"
1187> }
1188> ```
1189 
1190> [tab] PreToolUse
1191>
1192> Uses `hookSpecificOutput` for richer control: allow, deny, or escalate to the user. You can also modify tool input before it runs or inject additional context for Claude. See [PreToolUse decision control](#pretooluse-decision-control) for the full set of options.
1193>
1194> ```json theme={null}
1195> {
1196> "hookSpecificOutput": {
1197> "hookEventName": "PreToolUse",
1198> "permissionDecision": "deny",
1199> "permissionDecisionReason": "Database writes are not allowed"
1200> }
1201> }
1202> ```
1203 
1204> [tab] PermissionRequest
1205>
1206> Uses `hookSpecificOutput` to allow or deny a permission request on behalf of the user. When allowing, you can also modify the tool's input or apply permission rules so the user isn't prompted again. See [PermissionRequest decision control](#permissionrequest-decision-control) for the full set of options.
1207>
1208> ```json theme={null}
1209> {
1210> "hookSpecificOutput": {
1211> "hookEventName": "PermissionRequest",
1212> "decision": {
1213> "behavior": "allow",
1214> "updatedInput": {
1215> "command": "npm run lint"
1216> }
1217> }
1218> }
1219> }
1220> ```
12211221 
12221222For extended examples including Bash command validation, prompt filtering, and auto-approval scripts, see [What you can automate](https://code.claude.com/docs/en/hooks-guide.md#what-you-can-automate) in the guide and the [Bash command validator reference implementation](https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py).
12231223 
from line 1652
16521652 
16531653This example strips markdown formatting from Claude's responses for a plain-text display. The script reads each batch from stdin, removes bold markers and inline code backticks from `delta`, and returns the result as `displayContent`.
16541654 
1655**macOS/Linux**
1656 
1657Register a command hook for the event in your settings file:
1658 
1659```json theme={null}
1660{
1661 "hooks": {
1662 "MessageDisplay": [
1663 {
1664 "hooks": [
1665 {
1666 "type": "command",
1667 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.sh",
1668 "args": []
1669 }
1670 ]
1671 }
1672 ]
1673 }
1674}
1675```
1676 
1677Save this script to `.claude/hooks/plain-display.sh` in your project and make it executable with `chmod +x`:
1678 
1679```bash theme={null}
1680#!/bin/bash
1681jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'
1682```
1683 
1684**Windows (PowerShell)**
1685 
1686Register a command hook that runs the script through PowerShell:
1687 
1688```json theme={null}
1689{
1690 "hooks": {
1691 "MessageDisplay": [
1692 {
1693 "hooks": [
1694 {
1695 "type": "command",
1696 "command": "powershell.exe",
1697 "args": [
1698 "-NoProfile",
1699 "-ExecutionPolicy",
1700 "Bypass",
1701 "-File",
1702 "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.ps1"
1703 ]
1704 }
1705 ]
1706 }
1707 ]
1708 }
1709}
1710```
1711 
1712The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
1713 
1714Save this script to `.claude/hooks/plain-display.ps1` in your project:
1715 
1716```powershell theme={null}
1717$batch = [Console]::In.ReadToEnd() | ConvertFrom-Json
1718$text = $batch.delta -replace '\*\*', '' -replace '`', ''
1719@{
1720 hookSpecificOutput = @{
1721 hookEventName = "MessageDisplay"
1722 displayContent = $text
1723 }
1724} | ConvertTo-Json
1725```
1655> [tab] macOS/Linux
1656>
1657> Register a command hook for the event in your settings file:
1658>
1659> ```json theme={null}
1660> {
1661> "hooks": {
1662> "MessageDisplay": [
1663> {
1664> "hooks": [
1665> {
1666> "type": "command",
1667> "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.sh",
1668> "args": []
1669> }
1670> ]
1671> }
1672> ]
1673> }
1674> }
1675> ```
1676>
1677> Save this script to `.claude/hooks/plain-display.sh` in your project and make it executable with `chmod +x`:
1678>
1679> ```bash theme={null}
1680> #!/bin/bash
1681> jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'
1682> ```
1683 
1684> [tab] Windows (PowerShell)
1685>
1686> Register a command hook that runs the script through PowerShell:
1687>
1688> ```json theme={null}
1689> {
1690> "hooks": {
1691> "MessageDisplay": [
1692> {
1693> "hooks": [
1694> {
1695> "type": "command",
1696> "command": "powershell.exe",
1697> "args": [
1698> "-NoProfile",
1699> "-ExecutionPolicy",
1700> "Bypass",
1701> "-File",
1702> "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.ps1"
1703> ]
1704> }
1705> ]
1706> }
1707> ]
1708> }
1709> }
1710> ```
1711>
1712> The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
1713>
1714> Save this script to `.claude/hooks/plain-display.ps1` in your project:
1715>
1716> ```powershell theme={null}
1717> $batch = [Console]::In.ReadToEnd() | ConvertFrom-Json
1718> $text = $batch.delta -replace '\*\*', '' -replace '`', ''
1719> @{
1720> hookSpecificOutput = @{
1721> hookEventName = "MessageDisplay"
1722> displayContent = $text
1723> }
1724> } | ConvertTo-Json
1725> ```
17261726 
17271727Batches with no markdown pass through unchanged. If the script fails, for example because `jq` is missing, Claude Code displays the original text and notes the failure only in [debug output](#debug-hooks), not in the session.
17281728 
from line 3353
33533353 
33543354Write the matcher as an exact name, a `|`-separated list such as `claude-opus-4-6|claude-opus-5`, or a regular expression such as `.*opus.*`. This example uses an exact-name matcher and also checks `to_model` from the hook input, so it refuses a switch to Opus 4.6 by exiting with code 2 and lets any other target through:
33553355 
3356**macOS/Linux**
3357 
3358The command checks `to_model` with `jq`:
3359 
3360```json theme={null}
3361{
3362 "hooks": {
3363 "PreModelSwitch": [
3364 {
3365 "matcher": "claude-opus-4-6",
3366 "hooks": [
3367 {
3368 "type": "command",
3369 "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
3370 }
3371 ]
3372 }
3373 ]
3374 }
3375}
3376```
3377 
3378**Windows (PowerShell)**
3379 
3380Register a command hook that runs a script through PowerShell:
3381 
3382```json theme={null}
3383{
3384 "hooks": {
3385 "PreModelSwitch": [
3386 {
3387 "matcher": "claude-opus-4-6",
3388 "hooks": [
3389 {
3390 "type": "command",
3391 "command": "powershell.exe",
3392 "args": [
3393 "-NoProfile",
3394 "-ExecutionPolicy",
3395 "Bypass",
3396 "-File",
3397 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-opus-46.ps1"
3398 ]
3399 }
3400 ]
3401 }
3402 ]
3403 }
3404}
3405```
3406 
3407Save this script to `.claude/hooks/block-opus-46.ps1` in your project:
3408 
3409```powershell theme={null}
3410$hookInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
3411if ($hookInput.to_model -match 'opus-4-6') {
3412 [Console]::Error.WriteLine('Opus 4.6 is retired for this project. Use a newer model.')
3413 exit 2
3414}
3415exit 0
3416```
3356> [tab] macOS/Linux
3357>
3358> The command checks `to_model` with `jq`:
3359>
3360> ```json theme={null}
3361> {
3362> "hooks": {
3363> "PreModelSwitch": [
3364> {
3365> "matcher": "claude-opus-4-6",
3366> "hooks": [
3367> {
3368> "type": "command",
3369> "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
3370> }
3371> ]
3372> }
3373> ]
3374> }
3375> }
3376> ```
3377 
3378> [tab] Windows (PowerShell)
3379>
3380> Register a command hook that runs a script through PowerShell:
3381>
3382> ```json theme={null}
3383> {
3384> "hooks": {
3385> "PreModelSwitch": [
3386> {
3387> "matcher": "claude-opus-4-6",
3388> "hooks": [
3389> {
3390> "type": "command",
3391> "command": "powershell.exe",
3392> "args": [
3393> "-NoProfile",
3394> "-ExecutionPolicy",
3395> "Bypass",
3396> "-File",
3397> "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-opus-46.ps1"
3398> ]
3399> }
3400> ]
3401> }
3402> ]
3403> }
3404> }
3405> ```
3406>
3407> Save this script to `.claude/hooks/block-opus-46.ps1` in your project:
3408>
3409> ```powershell theme={null}
3410> $hookInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
3411> if ($hookInput.to_model -match 'opus-4-6') {
3412> [Console]::Error.WriteLine('Opus 4.6 is retired for this project. Use a newer model.')
3413> exit 2
3414> }
3415> exit 0
3416> ```
34173417 
34183418To confirm the hook works, run `/model claude-opus-4-6` from a session running a different model. Claude Code keeps the current model and reports that a PreModelSwitch hook blocked the switch, with your message as the reason.
34193419 
from line 3701
37013701 
37023702This example answers one recurring question for the user. An MCP server named `issue-tracker` asks for a project key in a form, and the hook fills in `DOCS`. The script accepts when `project_key` is the form's single field. For any other request it prints nothing, so the dialog appears.
37033703 
3704**macOS/Linux**
3705 
3706Register a command hook for the event in your settings file, with the server name as the matcher:
3707 
3708```json theme={null}
3709{
3710 "hooks": {
3711 "Elicitation": [
3712 {
3713 "matcher": "issue-tracker",
3714 "hooks": [
3715 {
3716 "type": "command",
3717 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.sh",
3718 "args": []
3719 }
3720 ]
3721 }
3722 ]
3723 }
3724}
3725```
3726 
3727Save this script to `.claude/hooks/answer-project-key.sh` in your project and make it executable with `chmod +x`:
3728 
3729```bash theme={null}
3730#!/bin/bash
3731input=$(cat)
3732fields=$(jq -c '.requested_schema.properties // {} | keys' <<<"$input")
3733 
3734if [ "$fields" = '["project_key"]' ]; then
3735 jq -n '{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept", content: {project_key: "DOCS"}}}'
3736fi
3737```
3738 
3739**Windows (PowerShell)**
3740 
3741Register a command hook that runs the script through PowerShell, with the server name as the matcher:
3742 
3743```json theme={null}
3744{
3745 "hooks": {
3746 "Elicitation": [
3747 {
3748 "matcher": "issue-tracker",
3749 "hooks": [
3750 {
3751 "type": "command",
3752 "command": "powershell.exe",
3753 "args": [
3754 "-NoProfile",
3755 "-ExecutionPolicy",
3756 "Bypass",
3757 "-File",
3758 "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.ps1"
3759 ]
3760 }
3761 ]
3762 }
3763 ]
3764 }
3765}
3766```
3767 
3768Save this script to `.claude/hooks/answer-project-key.ps1` in your project:
3769 
3770```powershell theme={null}
3771$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
3772$fields = @($request.requested_schema.properties.PSObject.Properties.Name)
3773 
3774if ($fields.Count -eq 1 -and $fields[0] -eq 'project_key') {
3775 @{
3776 hookSpecificOutput = @{
3777 hookEventName = "Elicitation"
3778 action = "accept"
3779 content = @{ project_key = "DOCS" }
3780 }
3781 } | ConvertTo-Json -Depth 3
3782}
3783```
3704> [tab] macOS/Linux
3705>
3706> Register a command hook for the event in your settings file, with the server name as the matcher:
3707>
3708> ```json theme={null}
3709> {
3710> "hooks": {
3711> "Elicitation": [
3712> {
3713> "matcher": "issue-tracker",
3714> "hooks": [
3715> {
3716> "type": "command",
3717> "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.sh",
3718> "args": []
3719> }
3720> ]
3721> }
3722> ]
3723> }
3724> }
3725> ```
3726>
3727> Save this script to `.claude/hooks/answer-project-key.sh` in your project and make it executable with `chmod +x`:
3728>
3729> ```bash theme={null}
3730> #!/bin/bash
3731> input=$(cat)
3732> fields=$(jq -c '.requested_schema.properties // {} | keys' <<<"$input")
3733>
3734> if [ "$fields" = '["project_key"]' ]; then
3735> jq -n '{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept", content: {project_key: "DOCS"}}}'
3736> fi
3737> ```
3738 
3739> [tab] Windows (PowerShell)
3740>
3741> Register a command hook that runs the script through PowerShell, with the server name as the matcher:
3742>
3743> ```json theme={null}
3744> {
3745> "hooks": {
3746> "Elicitation": [
3747> {
3748> "matcher": "issue-tracker",
3749> "hooks": [
3750> {
3751> "type": "command",
3752> "command": "powershell.exe",
3753> "args": [
3754> "-NoProfile",
3755> "-ExecutionPolicy",
3756> "Bypass",
3757> "-File",
3758> "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.ps1"
3759> ]
3760> }
3761> ]
3762> }
3763> ]
3764> }
3765> }
3766> ```
3767>
3768> Save this script to `.claude/hooks/answer-project-key.ps1` in your project:
3769>
3770> ```powershell theme={null}
3771> $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
3772> $fields = @($request.requested_schema.properties.PSObject.Properties.Name)
3773>
3774> if ($fields.Count -eq 1 -and $fields[0] -eq 'project_key') {
3775> @{
3776> hookSpecificOutput = @{
3777> hookEventName = "Elicitation"
3778> action = "accept"
3779> content = @{ project_key = "DOCS" }
3780> }
3781> } | ConvertTo-Json -Depth 3
3782> }
3783> ```
37843784 
37853785To confirm the hook works, start Claude Code with `claude --debug` and give Claude a task that makes the server ask for the project key. No dialog appears, and the [debug log](#debug-hooks) has a line that ends with `Elicitation resolved by hook: {"action":"accept","content":{"project_key":"DOCS"}}`.
37863786 
37873787 
Feedback