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