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
/
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
No line in this hunk matches that.