1# Hooks
2
3> [!NOTE]
4> This document was designed for both humans and agents.
5
6Hooks are user-defined shell scripts that run when various events happen during
7the agent lifecycle, allowing you to both build on top of Crush, customize
8its behavior, and exert deterministic control over an agent's wily behavior.
9
10Hooks are just shell commands, and were designed to be both simple and future
11forward.
12
13### Hot Hook Facts
14
15- Hooks are just shell commands
16- Hooks can be written in any language because theyβre just executables: Bash, Python, Node, Rust, Haskell, whatever
17- Hooks are Claude Code-compatible
18- Crush ships with a builtin `crush-hook` skill write, edit, and configure
19 hooks; just tell Crush how to configure Crush
20- Crush currently supports just one hook, `PreToolUse`, with plans to support
21 the full gamut; please let us know which hooks you'd like to see next
22- Hooks run in parallel for speed, but their results compose in config order
23 for determinism
24
25### Some things you can do with hooks:
26
27- Block "dangerous" commands: no more `git push -f` or `cabal init`
28- Rewrite tool input: turn `node` calls info `deno`, scrub secrets from
29 commands, rewrite all mentions of "Haskell" into "Haskell, The Best
30 Language", and so on
31- Inject context: add notes to the model's context whenever certain tools are
32 called. For example: "remember to run gofumpt after editing Go files"
33- Auto-approve tools: skip the permission prompt for bash commands that
34 you know are safe
35- Log certain tool calls
36
37β¦And lots more. Show us what you're building!
38
39## Baby's First Hook
40
41Let's just dive into it and make a simple hook. This particular hook will
42disallow the use of Haskell (but we love you, Simon Peyton Jones).
43
44### Config
45
46The first thing we need to do is hook up our hook. Let's add the following to
47our **project-level** `crush.json`. Relative paths like `./no-haskell.sh` work
48here because the project root is your working directory. If you're configuring
49a global hook (`~/.config/crush/crush.json`), use an absolute path instead.
50
51```jsonc
52{
53 // As expected, hooks go in a "hooks" object.
54 "hooks": {
55 // PreToolUse is an event that fires before a tool is used.
56 "PreToolUse": [
57 {
58 // What tool do we want to hook into? In this case, Bash, because it
59 // runs the stuff we wanna block.
60 "matcher": "^bash$",
61
62 // The path to our actual hook script.
63 "command": "./no-haskell.sh",
64 },
65 ],
66 },
67}
68```
69
70Now, let's make our `no-haskell.sh` hook script.
71
72```bash
73#!/usr/bin/env bash
74
75# Disallow ghc, cabal, and stack. Pipe the bash command output
76# ($CRUSH_TOOL_INPUT_COMMAND) to grep and match on a regexp.
77if echo "$CRUSH_TOOL_INPUT_COMMAND" | grep -qE '(^| )((ghc|cabal|stack)(\.exe)?)( |$)'; then
78
79 # Someone is trying to use Haskell. Let's send a message back to the model
80 # and user explaining why we're blocking this. Note that we send all feedback
81 # like this to stderr.
82 echo "No Haskell allowed, kiddo." >&2
83
84 # Now, block the tool call by exiting with code 2.
85 exit 2
86fi
87```
88
89That's basically it. For the full guide on how hooks work, however, read on.
90
91---
92
93## Execution model
94
95Hooks run through Crush's embedded POSIX shell (`mvdan.cc/sh`) β the same
96interpreter the `bash` tool uses. Inline commands and shebang-less scripts
97execute in-process; scripts with a `#!` shebang dispatch to the named
98interpreter via `os/exec`. This contract is identical on macOS, Linux, and
99Windows.
100
101What this means in practice:
102
103- **Windows without Unix tooling**: inline shell (`echo`, pipelines, `jq`,
104 `grep`), shebang-less `.sh` scripts, inline PowerShell
105 (`powershell -Command β¦`), and `.exe` invocations all work out of the box
106 with no WSL, Git Bash, Cygwin, or MSYS required.
107- **PowerShell scripts** (`.ps1`) are not auto-dispatched by extension.
108 Invoke them explicitly: `powershell -File ./audit.ps1` (or
109 `pwsh -File ./audit.ps1`).
110- **Shebang'd scripts** require the named interpreter on `PATH`. Git for
111 Windows ships `bash.exe`, which makes `#!/bin/bash` and
112 `#!/usr/bin/env bash` scripts work on Windows the same way they do on
113 Unix. CRLF line endings in the shebang line are tolerated.
114- **Permissive shebang fallback**: if the absolute path in a shebang
115 doesn't exist (e.g. `#!/bin/bash` on Windows), Crush falls back to a
116 `PATH` lookup of the base name (`bash`) before giving up. A debug-level
117 log records the fallback. If the interpreter isn't on `PATH` either, the
118 hook fails cleanly as a non-blocking warning and the agent proceeds as
119 "no opinion".
120- **Environment**: every hook sees `CRUSH=1`, `AGENT=crush`, and
121 `AI_AGENT=crush` on top of the `CRUSH_*` hook-specific variables. These
122 three markers are guaranteed and match what the `bash` tool sets, so
123 scripts that detect "am I being run by an AI agent?" behave the same in
124 both contexts.
125- **Timeout behavior**: when a hook exceeds its timeout, Crush cancels the
126 context and waits a short grace period (~1s) for the interpreter to
127 yield. If the hook still hasn't returned, Crush abandons it, logs a
128 warning, and treats the result as "no opinion" so the agent can proceed.
129 Long-running work should honor context cancellation or run in a
130 subprocess via a shebang.
131
132## Configuration
133
134Hooks can be added to your `crush.json` (or `.crush.json`) at both the global
135and project-level, with project level hooks taking precedence.
136
137```jsonc
138{
139 "hooks": {
140 "PreToolUse": [
141 {
142 "matcher": "bash", // regex tested against the tool name
143 "command": "./hooks/my-hot-hook.sh", // the path to the hook
144 "timeout": 10, // in seconds; default 30
145 },
146 ],
147 },
148}
149```
150
151> [!IMPORTANT]
152> The `command` is resolved relative to your **current working directory** β
153> not relative to the config file. Relative paths like `./hooks/whatever.sh`
154> work fine in project-level `crush.json` because the project root is also
155> your working directory. For **global** config (`~/.config/crush/`),
156> however, you must use either an absolute path or an inline command:
157>
158> ```jsonc
159> // Global ~/.config/crush/crush.json
160> {
161> "hooks": {
162> "PreToolUse": [
163> {
164> "command": "/home/you/.config/crush/hooks/no-haskell.sh"
165> // or use an inline command:
166> // "command": "echo '{\"decision\":\"allow\"}'"
167> }
168> ]
169> }
170> }
171> ```
172
173Remember, hooks will run in parallel but resolve in config order. Last hook
174wins when rewriting input, but first deny wins when blocking.
175
176## Events
177
178Here are the events you can hook into (spoiler: there's currently just one):
179
180### PreToolUse
181
182This hook fires before every tool call. Use it to block dangerous commands,
183enforce policies, rewrite tool input, inject context the model should see, log
184stuff, and so on.
185
186**Matched against**: the tool name (e.g. `bash`, `edit`, `write`,
187`mcp_github_create_pull_request`).
188
189> [!NOTE]
190> Event names are case insensitive and snake-caseable, so `PreToolUse`,
191> `pretooluse`, `PRETOOLUSE`, `pre_tool_use`, and `PRE_TOOL_USE` all work.
192
193**Scope**: `PreToolUse` only fires on the **top-level agent's** tool calls.
194Sub-agents (the `agent` task tool, `agentic_fetch`, etc.) run without hook
195interception so a single delegated turn doesn't trigger your hook N times. The
196outer sub-agent tool call itself _is_ hooked, so policy like "never let the
197agent spawn sub-agents" still works.
198
199Hooks are keyed by event name. Only `command` is required, and you can omit
200`matcher` to match all tools.
201
202## Building Hooks
203
204When a hook fires, Crush:
205
2061. Filters hooks whose `matcher` regex matches the tool name (no matcher = match
207 all).
2082. Deduplicates by `command` (identical commands run once).
2093. Runs all matching hooks **in parallel** through Crush's embedded POSIX
210 shell (see [Execution model](#execution-model)).
2114. Waits for all to finish (or time out), then aggregates results **in config
212 order**: deny wins over allow, allow wins over none; `updated_input` patches
213 shallow-merge in order.
2145. Applies the result **before** permission checks. If the aggregated decision
215 is `deny`, the tool call is blocked and you never see a permission prompt
216 for it. If it's `allow`, Crush treats that as affirmative pre-approval and
217 also skips the prompt. Silence (no decision) falls through to the normal
218 permission flow.
219
220Note that you can omit `matcher` and match in your shell script instead,
221however you'll incur some additional overhead as Crush will still parse and
222run each hook.
223
224### Input
225
226Each hook receives data two ways: environment variables and stdin (as JSON).
227Environment variables are typically easier to work with, with JSON being
228available when input is more complex.
229
230#### Environment Variables
231
232The available environment variables are:
233
234| Variable | Description |
235| ---------------------------- | ---------------------------------------------- |
236| `CRUSH` | Always `1` when running under Crush. |
237| `AGENT` | Always `crush`. |
238| `AI_AGENT` | Always `crush`. |
239| `CRUSH_EVENT` | The hook event name (e.g. `PreToolUse`). |
240| `CRUSH_TOOL_NAME` | The tool being called (e.g. `bash`). |
241| `CRUSH_SESSION_ID` | Current session ID. |
242| `CRUSH_CWD` | Working directory. |
243| `CRUSH_PROJECT_DIR` | Project root directory. |
244| `CRUSH_TOOL_INPUT_COMMAND` | For `bash` calls: the shell command being run. |
245| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path. |
246
247The `CRUSH`, `AGENT`, and `AI_AGENT` markers are also set by the `bash`
248tool, so a script can detect "am I running under Crush?" the same way in
249either context.
250
251#### JSON
252
253Standard input provides the full context as JSON:
254
255```jsonc
256{
257 "event": "PreToolUse", // Hook event name
258 "session_id": "313909e", // Current session ID
259 "cwd": "/home/user/project", // Working directory
260 "tool_name": "bash", // The tool being called
261 "tool_input": { "command": "rm -rf /" }, // The tool's input
262}
263```
264
265Note that `tool_input` field contains the raw JSON the model sent to the tool.
266
267To parse the stdin JSON in your hook script, read from stdin and use a tool like
268`jq`:
269
270```bash
271#!/usr/bin/env bash
272read -r input
273tool_name=$(echo "$input" | jq -r '.tool_name')
274command=$(echo "$input" | jq -r '.tool_input.command // empty')
275```
276
277You can also use tools like Python:
278
279```python
280#!/usr/bin/env python3
281import json, sys
282
283data = json.load(sys.stdin)
284tool_name = data.get("tool_name", "")
285command = data.get("tool_input", {}).get("command", "")
286```
287
288### Output
289
290Hooks communicate back to Crush via **exit code** and `stdout`/`stderr`. The
291simplest way to do this is to return an error code and print additional context
292to stderr. For example:
293
294```bash
295# Here, error code 2 blocks the tool, using stderr as the reason:
296if some_bad_condition; then
297 echo "Blocked: reason here" >&2
298 exit 2
299fi
300```
301
302| Exit Code | Meaning |
303| --------- | ---------------------------------------------------------------- |
304| 0 | Success. Stdout is parsed as JSON (see fields below). |
305| 2 | **Block the tool.** Stderr is used as the deny reason (no JSON). |
306| 49 | **Halt the turn.** Stderr is used as the halt reason (no JSON). |
307| Other | Non-blocking error. Logged and ignored β the tool call proceeds. |
308
309The difference between exit 2 and exit 49:
310
311- **Exit 2** blocks the current tool call. The agent sees the error and can try
312 something else.
313- **Exit 49** halts the whole turn. The agent doesn't get to respond further;
314 the user takes over. Use this when something is wrong enough that the agent
315 shouldn't keep trying. 49 sits in an empty slice of the exit-code space β
316 between the generic-error range (1-30), the BSD `sysexits.h` range (64-78),
317 and the killed-by-signal range (128+) β so it can't be hit by accident.
318
319That said, if you need more control, or if you need to rewrite input, you can
320use JSON on stdout. Exit 0 and print a JSON object to provide context, update
321the input, or still deny/halt with a reason:
322
323```jsonc
324{
325 "version": 1, // Output envelope version. Optional; defaults to 1.
326 "decision": "allow", // "allow", "deny", or null. Omit for no opinion.
327 "halt": false, // If true, halts the turn entirely.
328 "reason": "LGTM", // Shown when denying or halting.
329 "context": "Scrubbed secrets", // String or array of strings. Appended to what the model sees.
330 "updated_input": { "command": "β¦" }, // Shallow-merged into the tool's input before execution.
331}
332```
333
334`version` is an optional integer at the top of the envelope. It defaults to `1`
335if omitted. Unknown higher versions are still parsed; the field exists so the
336envelope can evolve without a compatibility shim.
337
338`decision: "allow"` is **affirmative**: it pre-approves the tool call and
339bypasses the permission prompt entirely. Silence (no `decision`, or
340`decision: null`) means "no opinion" β the tool still goes through the
341normal permission flow. Use `"allow"` when you want to auto-approve; omit it
342when you only want to inject context or rewrite input without also vouching
343for the call.
344
345`updated_input` is a shallow-merge patch. Keys you include overwrite matching
346keys in `tool_input`; keys you don't include are preserved. If the model called
347`bash` with `{"command": "npm test", "timeout": 60000}` and your hook returns
348`{"updated_input": {"command": "bun test"}}`, the tool runs with
349`{"command": "bun test", "timeout": 60000}` β the timeout isn't dropped. The
350merge is shallow: nested objects are replaced wholesale, not deep-merged.
351
352`halt: true` stops the turn entirely. The agent doesn't get to respond further;
353the user takes over. The exit-code shorthand is `exit 49` with stderr as the
354reason.
355
356`context` accepts either a string or an array of strings. Use the string form
357for a single observation; use the array form when a hook produces multiple
358distinct notes and you'd rather not concatenate them by hand. Empty strings and
359empty array entries are dropped.
360
361Here's a full shell script that produces this JSON:
362
363```bash
364#!/usr/bin/env bash
365# Example: rewrite a bash command using RTK
366
367read -r input
368original_cmd=$(echo "$input" | jq -r '.tool_input.command')
369rewritten=$(secret-scrubber rewrite "$original_cmd")
370
371cat <<EOF
372{
373 "decision": "allow",
374 "context": "Scrubbed secrets",
375 "updated_input": {"command": "$rewritten"}
376}
377EOF
378```
379
380### Multiple Hooks
381
382Hooks run in parallel, but their results compose in config order. Whichever hook
383finishes first doesn't get to "win" by virtue of timing; composition is
384deterministic based on the order hooks appear in `crush.json`.
385
386When multiple hooks match the same tool call:
387
388- If **any** hook denies, the tool call is blocked. `reason` values are
389 concatenated in config order (newline-separated).
390- If **any** hook halts, the turn ends after the tool call is blocked.
391- If no hook denies or halts but at least one allows, the tool call proceeds
392 **and the permission prompt is skipped**.
393- `context` values are concatenated in config order. Strings and arrays compose
394 uniformly β each string becomes one entry, and array entries are flattened in.
395- `updated_input` patches shallow-merge in config order against the original
396 tool input. Later hooks override earlier ones on colliding keys. If denied or
397 halted, `updated_input` patches are ignored.
398
399### Timeouts
400
401If a hook exceeds its timeout, Crush cancels its context and treats the
402result as a non-blocking error so the tool call proceeds. The default
403timeout is 30 seconds. Shebang-dispatched subprocesses are killed through
404`exec.CommandContext`; in-process hooks get a short grace period to yield
405and are then abandoned (the agent moves on regardless). Long-running work
406should honor context cancellation or run out-of-process via a shebang.
407
408## Examples
409
410### Block destructive commands
411
412Prevent the agent from running `rm -rf` in bash:
413
414```json
415{
416 "hooks": {
417 "PreToolUse": [
418 {
419 "matcher": "^bash$",
420 "command": "./hooks/no-rm-rf.sh"
421 }
422 ]
423 }
424}
425```
426
427`hooks/no-rm-rf.sh`:
428
429```bash
430#!/usr/bin/env bash
431# Block rm -rf commands in the bash tool. Otherwise stay silent so the
432# normal permission flow runs.
433
434if echo "$CRUSH_TOOL_INPUT_COMMAND" | grep -qE 'rm\s+-(rf|fr)\s+/'; then
435 echo "Refusing to run rm -rf against root" >&2
436 exit 2
437fi
438
439exit 0
440```
441
442### Auto-approve read-only tools
443
444Skip the permission prompt for tools that can't change anything. The hook
445returns `decision: "allow"`, which tells Crush to pre-approve the call:
446
447```jsonc
448{
449 "hooks": {
450 "PreToolUse": [
451 {
452 "matcher": "^(view|ls|grep|glob)$",
453 "command": "echo '{\"decision\":\"allow\"}'",
454 },
455 ],
456 },
457}
458```
459
460No script file needed β the command is inline. Every `view`/`ls`/`grep`/`glob`
461call now runs without prompting. Add the `bash` tool to this list at your own
462risk; consider a more targeted allowlist instead:
463
464```bash
465#!/usr/bin/env bash
466# hooks/safe-bash.sh β auto-approve read-only bash commands.
467
468case "$CRUSH_TOOL_INPUT_COMMAND" in
469 ls*|cat*|grep*|rg*|echo*|pwd*)
470 echo '{"decision":"allow"}'
471 ;;
472 *)
473 # Silent β fall through to the normal permission prompt.
474 exit 0
475 ;;
476esac
477```
478
479### Inject context into file writes
480
481Add a reminder to the model whenever it writes a Go file:
482
483```json
484{
485 "hooks": {
486 "PreToolUse": [
487 {
488 "matcher": "^(edit|write|multiedit)$",
489 "command": "./hooks/go-context.sh"
490 }
491 ]
492 }
493}
494```
495
496`hooks/go-context.sh`:
497
498```bash
499#!/usr/bin/env bash
500# Remind the model about Go formatting when editing .go files.
501# Emit context only; stay silent on `decision` so the normal permission
502# prompt still runs for edits/writes.
503
504if [[ "$CRUSH_TOOL_INPUT_FILE_PATH" == *.go ]]; then
505 echo '{"context": "Remember: run gofumpt after editing Go files."}'
506else
507 echo '{}'
508fi
509```
510
511### Block all MCP tools
512
513The `command` can be inline. This one-liner matches all MCP tools and blocks
514them:
515
516```jsonc
517{ "matcher": "^mcp_", "command": "echo 'MCP tools are disabled' >&2; exit 2" }
518```
519
520### Log every tool call
521
522With no `matcher` this fires for every tool. It exits 0 with no stdout so the
523tool call always proceeds.
524
525```jsonc
526{ "command": "echo \"$(date -Iseconds) $CRUSH_TOOL_NAME\" >> ./tools.log" }
527```
528
529### A real-world Example:
530
531For a more practical example, see [`rtk-rewrite.sh`](./examples/rtk-rewrite.sh),
532which demonstrates how to rewrite tool input using
533[RTK](https://github.com/rtk-ai/rtk) to save tokens.
534
535### Using other languages
536
537Hooks aren't limited to shell scripts: any executable works. Here's the same
538"block rm -rf" example in some other languages.
539
540#### Lua
541
542`{"matcher": "^bash$", "command": "lua ./hooks/no-rm-rf.lua"}`
543
544```lua
545local input = io.read("*a")
546local tool_input = input:match('"command":"(.-)"') or ""
547
548if tool_input:match("rm%s+%-[rf][rf]%s+/") then
549 io.stderr:write("Refusing to run rm -rf against root\n")
550 os.exit(2)
551end
552```
553
554#### JavaScript
555
556`{"matcher": "^bash$", "command": "node ./hooks/no-rm-rf.js"}`
557
558```js
559let input = "";
560process.stdin.on("data", (chunk) => (input += chunk));
561process.stdin.on("end", () => {
562 const { tool_input: toolInput } = JSON.parse(input);
563
564 if (/rm\s+-[rf]{2}\s+\//.test(toolInput.command)) {
565 process.stderr.write("Refusing to run rm -rf against root\n");
566 process.exit(2);
567 }
568});
569```
570
571---
572
573## Claude Code compatibility
574
575Crush hooks are broadly compatible with [Claude Code
576hooks](https://docs.claude.com/en/docs/claude-code/hooks): the config shape,
577stdin payload, output envelope, and exit codes line up so most Claude Code
578hooks run under Crush unchanged. This document covers the Crush-specific API
579only β anything not documented here isn't guaranteed to work.
580
581One intentional divergence: Crush treats `updated_input` as a shallow-merge
582patch against the original `tool_input` rather than a full replacement. Keys
583you omit are preserved. See [Output](#output) for details.
584
585---
586
587## Reference
588
589This is the official reference of the narrative above. If prose and this section
590disagree, the prose should be presumed canonical for intent, while this section
591is canonical for shape.
592
593Both the stdin payload and the output envelope have **common fields** that apply
594to every event and **per-event fields** that only some events recognize. When an
595event doesn't understand a field, it's ignored.
596
597### Hook config
598
599Each entry under a `hooks.<EventName>` array:
600
601```jsonc
602{
603 // string. Optional. Regex tested against the tool name. Omit to match all.
604 "matcher": "^bash$",
605
606 // string. Required. Shell command to run.
607 "command": "./hooks/my-hook.sh",
608
609 // number. Optional. Seconds before the hook is killed. Defaults to 30.
610 "timeout": 10,
611}
612```
613
614### Stdin payload (common)
615
616Present in every hook event:
617
618```jsonc
619{
620 // string. Hook event name.
621 "event": "PreToolUse",
622
623 // string. Current session ID.
624 "session_id": "313909e",
625
626 // string. Working directory when invoked.
627 "cwd": "/home/user/project",
628}
629```
630
631### Stdin payload β PreToolUse
632
633Extends the common payload:
634
635```jsonc
636{
637 // ...common fields...
638
639 // string. The tool being called.
640 "tool_name": "bash",
641
642 // object. Raw JSON input the model sent to the tool. Shape is per-tool.
643 "tool_input": {
644 "command": "npm test",
645 },
646}
647```
648
649### Output envelope (common)
650
651Fields a hook may print to stdout on exit 0. All are optional and apply to every
652event:
653
654```jsonc
655{
656 // number. Defaults to 1. Unknown higher values still parse; exists for
657 // forward-compat.
658 "version": 1,
659
660 // boolean. If true, ends the turn entirely. User takes over.
661 "halt": false,
662
663 // string. Shown when denying (to the model) or halting (to the model and
664 // user).
665 "reason": "not allowed",
666
667 // string | string[]. Appended to what the model sees. Empty entries are
668 // dropped.
669 "context": "Rewrote with RTK",
670}
671```
672
673### Output envelope β PreToolUse
674
675Extends the common envelope:
676
677```jsonc
678{
679 // ...common fields...
680
681 // "allow" | "deny" | null. null/omitted = no opinion, the tool still goes
682 // through the normal permission prompt. "allow" is affirmative: pre-approves
683 // the tool call and bypasses the prompt. "deny" blocks the call; the model
684 // sees the error and may try something else.
685 "decision": "allow",
686
687 // object. Shallow-merge patch against tool_input. Nested objects are
688 // replaced wholesale, not deep-merged.
689 "updated_input": {
690 "command": "bun test",
691 },
692}
693```
694
695### Exit codes
696
697| Code | Meaning |
698| ----- | ------------------------------------------------------------------------ |
699| `0` | Success. Stdout is parsed as the output envelope. |
700| `2` | Block this tool call. Stderr becomes the deny reason. Stdout is ignored. |
701| `49` | Halt the whole turn. Stderr becomes the halt reason. Stdout is ignored. |
702| other | Non-blocking error. Logged and ignored; the tool call proceeds. |
703
704Exit `2` only applies to events that can block something. On events where
705there's nothing to block, it's treated as a non-blocking error.
706
707### Aggregation
708
709When multiple hooks match the same event, results compose in **config order**.
710
711Universal rules:
712
7131. `halt` is sticky: if any hook halts, the turn ends.
7142. `reason` values concatenate with `\n` in config order. Halt-only hooks
715 without a deny still contribute their reason.
7163. `context` values concatenate with `\n` in config order. String entries and
717 array entries flatten uniformly.
718
719PreToolUse-specific rules:
720
7214. `decision` precedence: `deny` > `allow` > `null`. First deny determines the
722 outcome; subsequent allows don't override. If the final aggregated decision
723 is `allow`, Crush pre-approves the tool call and skips the permission
724 prompt. If it's `null` (no hook allowed), the tool goes through the normal
725 permission flow.
7265. `updated_input` patches shallow-merge sequentially against the original
727 `tool_input`. Later patches override earlier ones on colliding keys. Patches
728 are **ignored** if the final decision is deny or halt.
729
730### Environment variables
731
732See [Environment Variables](#environment-variables) above for the full list.
733
734---
735
736## Whatcha think?
737
738We'd love to hear your thoughts on this project. Need help? We gotchu. You can
739find us on:
740
741- [Twitter](https://twitter.com/charmcli)
742- [Slack](https://charm.land/slack)
743- [Discord](https://charm.land/discord)
744- [The Fediverse](https://mastodon.social/@charmcli)
745- [Bluesky](https://bsky.app/profile/charm.land)
746
747---
748
749Part of [Charm](https://charm.land).
750
751<a href="https://charm.land/"><img alt="The Charm logo" width="400" src="https://stuff.charm.sh/charm-banner-softy.jpg" /></a>
752
753<!--prettier-ignore-->
754Charmηη±εΌζΊ β’ Charm loves open source