skribbl
productpricingfree!questionswriting
download
guides · 7 August 2026 · 12 min read

Claude Code hooks: a practical guide with examples

Every event, the payload it carries, and the exit code that blocks a tool call.

skribbl/writing/guides
the events, and what to do with them

Claude Code fires shell hooks at named points in a session: session start, prompt submit, before and after every tool call, on stop and on notification. A guide to each event, the JSON it hands you on stdin, the exit codes that block a tool call, and the rule that keeps a broken hook from bricking your agent.

what a hook is

A Claude Code hook is a shell command the CLI runs for you at a named point in a session: before a tool call, after a tool call, when you submit a prompt, when the agent stops. Claude Code hands the command a JSON object describing what just happened on stdin. What the command does with that, and what it exits with, decides whether the session carries on normally, carries on with something extra injected, or stops.

The important thing is the direction of control. A hook is not a prompt. You are not asking the model to remember to run the formatter, you are running the formatter yourself, from outside the model, at a moment the CLI guarantees. Everything you move from the prompt into a hook becomes deterministic, and the prompt gets shorter at the same time. That trade is the whole reason hooks exist.

Three things people reach for hooks to do, and they are three different shapes of hook:

  • Something that must happen every time. Run the formatter after every edit, write a line to an audit log after every command. These are PostToolUse hooks and they cannot fail in a way that matters.
  • Something that must never happen. A command you refuse to let an agent run. This is a PreToolUse hook, it is the only shape that can actually stop the CLI, and the section on exit codes below is entirely about getting it right.
  • Something you want to know about. A desktop notification when the agent finishes or needs you. These are Stop and Notification hooks, and they are the ones that pay off most when you are running more than one agent at a time, because that is the situation where you stop noticing that one of them has been waiting.
Claude Code's hook surface has grown quickly, and the exact set of events differs between versions. Everything below that we verified, we verified against a real installed CLI on the date stated. Everything else is described in general terms and marked. Before you rely on a specific event name, run /hooks inside a session and read what your own version lists.

where hooks are configured

Hooks live under a hooks key in a settings file. There are three you will use, and the difference between them is who they follow around:

~/.CLAUDE/SETTINGS.JSONYour user settings. Applies to every project on this machine. The right place for a formatter you always want and a notification you always want.
.CLAUDE/SETTINGS.JSONProject settings, inside the repository. Committed, so everyone working on this repo gets them. The right place for the guard that stops an agent touching production config.
.CLAUDE/SETTINGS.LOCAL.JSONProject settings that stay on this machine. Gitignored. The right place for anything with a local path or a personal preference in it.

Organisations can also ship managed settings that take precedence over all three, and plugins can bundle their own hooks. If you are on a managed install and a hook you wrote is being ignored, that is the first thing to check. The precedence order and the managed settings path are both version-dependent enough that you should read them off your own install rather than off this page.

The shape is the same everywhere. An event name, a list of matchers, a list of commands:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

The nesting reads oddly the first time. The outer array is a list of matcher groups, and each group has its own inner hooks array of commands to run when that matcher matches. Use separate groups when different tools need different commands.

The matcher matches the tool name for PreToolUse and PostToolUse: Bash, Edit, Write, Read, and MCP tools under names like mcp__servername__toolname. Plain names separated by | are treated as alternatives. An empty matcher, or no matcher at all, matches everything. Other events interpret the matcher differently or ignore it; check /hooks for what yours does.

$CLAUDE_PROJECT_DIR is exported into the hook process and points at the project root, which is what lets a committed project hook reference a script by a path that works on everyone's machine. Use it. A hook whose command is a bare relative path is a hook that breaks the moment somebody runs the agent from a subdirectory.

The failure this prevents: a hook that works for you and silently does nothing for everyone else on the team, because the path resolved against your home directory.

the events, one by one

The events below are the ones we install ourselves and have watched fire against a real CLI. Recent versions carry considerably more than these, including events for subagents, worktree creation, file changes and config changes. Take this as the stable core, not as the complete list.

SESSIONSTARTA session begins or resumes. Useful for injecting context the agent should have from the first turn: the current branch, an open incident, the fact that this checkout is one of four. Its stdout can add context rather than only reporting.
USERPROMPTSUBMITYou pressed enter, before Claude sees the prompt. Useful for appending standing context, and for refusing prompts outright. This is one of the events where blocking actually blocks.
PRETOOLUSEA tool is about to run, with the arguments it will run with. The only place a tool call can be stopped, and therefore where every guard belongs.
POSTTOOLUSEA tool finished successfully, with its result. Where formatters, linters and audit logs go. It cannot stop the call, which already happened, but it can tell Claude the result was unacceptable.
POSTTOOLUSEFAILUREA tool failed. Measured on Claude Code 2.1.220: a failed call fires this INSTEAD of PostToolUse, and the success payload carries no error flag at all. If you are counting failures, you must install this one; you cannot infer it from PostToolUse.
NOTIFICATIONClaude Code wants your attention: a permission prompt, or a long idle wait. The natural home for a desktop notification.
STOPClaude finished responding and the turn is over. Good for notifications, good for a final check. It can refuse to let the turn end, which is powerful and easy to misuse.
SUBAGENTSTOPA subagent finished. Fires while the parent turn continues, so treat it as progress rather than completion.
SESSIONENDThe session is going away. Cleanup only. Assume a short budget; nothing slow belongs here.
PRECOMPACTThe context window is about to be compacted. Useful as a marker for the moment right before the agent forgets the early part of the conversation.
PERMISSIONREQUESTA tool needs a permission decision. Probed on 2.1.212: it did not fire at all in a non-interactive run, while PreToolUse fired once in the identical scenario. Keep it for observation, not for decisions.

One finding worth carrying with you, because it saved us a day: a settings file naming an event the CLI does not know is ignored silently, not rejected. Probed on 2.1.220. That is convenient for forward compatibility, since listing an event a future version adds costs nothing today. It is also why a hook that does nothing gives you no error to search for. If an event name is not firing, suspect the spelling before you suspect the script.

the JSON handed to your hook on stdin

Every hook is spawned with a single JSON object on stdin. Read it with cat in shell, json.load(sys.stdin) in Python, or ignore it entirely if all you want is a side effect. There is no argument vector to parse; the payload is the interface.

These fields were confirmed present on a real CLI (2.1.212, macOS, probed 2026-07-26), on every event we watched:

{
  "session_id":      "…",
  "prompt_id":       "…",
  "transcript_path": "/Users/you/.claude/projects/<dir>/<session-id>.jsonl",
  "cwd":             "/Users/you/code/myapp",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse"
}

Tool events add tool_name, tool_input and tool_use_id. tool_input is the tool's own argument object, so for Bash it has a command key, and for Edit and Write it has a file_path. Those two are the fields nearly every useful hook reads.

The result field on a completed tool call is the one to check on your own version. We measured it as tool_output on 2.1.212, and the published reference has used tool_response. If your hook needs the result rather than the input, print the whole payload to a file once and read what actually arrived. Do not take either name on faith, including from us.

Other events carry their own extras: prompt on UserPromptSubmit, a source string on SessionStart saying whether this was a startup, a resume or a clear, a message on Notification. The reliable way to learn the exact shape for the event you care about is four lines:

#!/bin/sh
# .claude/hooks/dump.sh - install on the event you are learning, then delete it.
cat >> /tmp/claude-hook-payloads.jsonl
echo >> /tmp/claude-hook-payloads.jsonl
exit 0

Trigger the event once, read the file, then remove the hook. That takes about two minutes and it is authoritative for your version, which no document can be.

permission_mode deserves a note, because it has an asymmetry we walked into. The vocabulary the payload reports and the vocabulary the --permission-mode flag accepts are not identical: on 2.1.212 the flag value manual arrives in hook payloads as default, and default is not accepted as a flag value at all. If you are writing a hook whose behaviour depends on the mode, read it from the payload and test the mode you actually use.

exit codes, and which one blocks

Exit code 2 is the one that blocks. Everything else is a variation on "carry on". This is the single most misremembered fact about hooks, because in every other context a non-zero exit means failure and here it mostly does not.

EXIT 0Success. Claude Code reads your stdout and, if it is valid JSON of the shape it expects, acts on it. This is the channel for anything structured.
EXIT 2Blocking error. On the events that can block, the action does not happen and your stderr is shown to Claude as the reason it did not.
EXIT 1, 3, ANYTHING ELSENon-blocking error. The action proceeds regardless. Your message goes to the debug log and nowhere a human will look. Never use exit 1 hoping to stop something.

Which events actually honour exit 2 is the part to be careful about, because a hook that exits 2 on an event that cannot block just fails silently in a way that looks like it worked. The ones we rely on:

  • PreToolUse blocks the tool call. This is the important one.
  • UserPromptSubmit blocks the prompt before Claude sees it.
  • Stop refuses to let the turn end, which sends Claude back to work.
  • PostToolUse does not block, because the tool already ran. Exit 2 there surfaces your stderr to Claude as a complaint about the result. That is genuinely useful (a type error from a post-edit check lands in front of the model that caused it) but it is not prevention.
  • Reporting events like Notification, SessionStart and SessionEnd block nothing at all. Their exit code is essentially cosmetic.

The structured alternative to exit 2

Exiting 0 and printing JSON gives you more control than an exit code does. On PreToolUse, a decision object on stdout controls the call. We verified both shapes work on 2.1.212, which is why our own script emits the modern one and treats the legacy one as a free hedge against version skew:

# modern shape
{"hookSpecificOutput":{"hookEventName":"PreToolUse",
 "permissionDecision":"deny",
 "permissionDecisionReason":"migrations/ is append-only in this repo"}}

# legacy shape, also honoured on 2.1.212
{"decision":"block","reason":"…"}

permissionDecision takes allow, deny or ask, so a hook can pre-approve a call as well as refuse one. Printing nothing and exiting 0 leaves the normal permission flow untouched, which is the graceful fallback every hook should collapse to when it is unsure. Other fields exist for injecting context and passing a message to the user, and the set varies by version.

If you are thinking about hooks as an authority mechanism, that is a bigger subject than a single exit code and we wrote it up separately in who may command whom.

three worked examples

1. A formatter on PostToolUse

The canonical hook, and the one most worth having. After Claude edits or writes a file, run the formatter on it. Not because the model cannot format, but because the model formatting costs tokens on every single edit and the formatter is free and correct.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh",
            "timeout": 30 }
        ]
      }
    ]
  }
}
#!/bin/sh
# .claude/hooks/format.sh
set +e

payload=$(cat)
file=$(printf '%s' "$payload" | python3 -c \
  'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))' \
  2>/dev/null)

[ -n "$file" ] || exit 0
[ -f "$file" ] || exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
    npx --no-install prettier --write "$file" >/dev/null 2>&1
    ;;
  *.py)
    ruff format "$file" >/dev/null 2>&1
    ;;
esac

exit 0

Note the shape as much as the content. Every step that could fail is followed by a guard that exits 0, --no-install stops npx pausing to download something the repository does not have, and nothing writes to stdout, so nothing can be misread as a decision object.

The failure this prevents: a diff where half the changes are formatting the model did by hand and inconsistently, and the reviewer cannot see the actual change through them.

2. A guard on PreToolUse

The one hook worth writing on day one. It refuses a class of command outright, before it runs, without asking you.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh",
            "timeout": 10 }
        ]
      }
    ]
  }
}
#!/bin/sh
# .claude/hooks/guard.sh - exit 2 blocks the call, stderr tells Claude why.
set +e

payload=$(cat)
cmd=$(printf '%s' "$payload" | python3 -c \
  'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' \
  2>/dev/null)

# A parse failure means we do not know what the command is, so we allow it.
# See the fail-open section: a guard that blocks when confused blocks everything.
[ -n "$cmd" ] || exit 0

deny() {
  echo "Blocked by repo policy: $1" >&2
  exit 2
}

case "$cmd" in
  *"git push --force"*|*"git push -f"*)  deny "no force pushes from an agent" ;;
  *"git reset --hard"*)                  deny "reset --hard discards work the human has not seen" ;;
  *"rm -rf /"*)                          deny "absolute recursive delete" ;;
  *"DROP TABLE"*|*"TRUNCATE "*)          deny "destructive SQL" ;;
  *" .env"*|*"cat .env"*)                deny "environment files are not read by agents here" ;;
esac

exit 0

Two honest caveats. The stderr message matters: Claude reads it and will usually adapt rather than retry, so "no force pushes from an agent" produces better behaviour than a bare "denied". And this is pattern matching on a string. It stops accidents, which is most of what goes wrong. It does not stop a determined caller, because the same command can be written through a variant flag, an alias or a script. Treat it as a guardrail, never as a security boundary. The real boundary is what the agent can reach at all.

The failure this prevents: the one command in a thousand that destroys work nobody has reviewed yet. Not the model being malicious, just an agent tidying up a branch state it misread.

3. A desktop notification on Stop

The hook that changes how a day feels, and the reason is boring: an agent that finished three minutes ago and is sitting silently is three minutes of your life spent looking at another window.

{
  "hooks": {
    "Stop": [
      { "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh",
            "timeout": 10 }
      ]}
    ],
    "Notification": [
      { "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh",
            "timeout": 10 }
      ]}
    ]
  }
}
#!/bin/sh
# .claude/hooks/notify.sh - macOS. Use notify-send on Linux.
set +e

payload=$(cat)
dir=$(basename "$(pwd)")

case "$payload" in
  *'"hook_event_name"'*'"Notification"'*) title="Claude needs you: $dir" ;;
  *)                                      title="Claude finished: $dir" ;;
esac

osascript -e "display notification \"$dir\" with title \"$title\"" >/dev/null 2>&1
exit 0

The matcher is omitted on both events, so it fires on all of them, and the script tells them apart by reading hook_event_name out of the payload rather than by taking an argument. That is the pattern for one script registered against several events.

The failure this prevents: the specific waste of running two or three agents and discovering that one of them has been blocked on a permission prompt since before lunch. Notification hooks are how a multi-agent workspace stops needing you to poll it.

when a hook does not fire

Hooks fail quietly by design, which is right for production and infuriating while you are writing one. Work the list in this order, because it is roughly the order of how often each one is the answer.

  • Run /hooks in the session. It lists what this session actually loaded. If your hook is not in that list, nothing downstream matters and the problem is in the settings file, not the script.
  • Check the JSON is valid. A trailing comma invalidates the whole settings file, and what you lose is not just the hook, it is every other setting in it. python3 -m json.tool ~/.claude/settings.json settles it in one line.
  • Restart the session. Whether a running session picks up a mid-session settings edit is not something we have verified, and it may differ by version. Restarting removes the question.
  • Check the event name and the matcher. An unknown event name is ignored silently (measured on 2.1.220), so a typo produces no error anywhere. Matchers are case sensitive, and the tool is Bash, not bash.
  • Check the script runs at all. chmod +x it, confirm the shebang is correct, then run it by hand with a payload piped in.
  • Then use claude --debug. It prints each hook spawning, what it was given, what it returned and its exit code. This is where you find the answer when the first five did not.

The three that are not obvious

Your shell profile is polluting stdout. If your .bashrc or .zshrc unconditionally echoes anything, and the hook process sources it, that text lands on the hook's stdout ahead of your JSON and the whole payload is now unparseable. This produces the maddening symptom of a hook that works when you run it by hand and not when Claude runs it. Guard interactive output in your profile with a check for an interactive shell.

Something is holding stdout open. Claude Code reads a hook's stdout to EOF. If your hook backgrounds a child and that child inherits stdout, the pipe stays open until the child exits and the tool call waits. We hit this exactly: backgrounding our status POST was not enough, because the subshell still held the inherited descriptor. Redirect the subshell itself, not the command inside it: ( … ) </dev/null >/dev/null 2>&1 &. Symptom: everything works, everything is just slow, nothing is in any log.

The hook fires and its effect is invisible. A PostToolUse formatter that writes to a file leaves no trace in the transcript, so it looks identical to a hook that never ran. Log a line while developing, and take it out afterwards.

a hook is arbitrary code running as you

Every hook is a command executed with your user's full privileges, with no sandbox, no confirmation and no prompt, triggered by a decision an agent made. There is no permission dialog in front of it. A hook is not inspected, is not reviewed, and does not appear on screen when it runs. If a hook can delete your home directory, it will delete your home directory, and the agent that triggered it will report the tool call succeeded.

Which makes a short list non-negotiable:

  • Never accept a settings.json you did not read. Not from a blog post, not from a gist, not from a plugin, not from this page. A hooks block is a remote code execution primitive wearing a config file. Read every command string before it lands on your machine.
  • Quote every variable. A file path from tool_input is a string an agent chose, and an unquoted $file in a shell script is a command injection with extra steps. "$file", always, everywhere.
  • Assume the payload is attacker-influenced. Prompt content, file contents and tool arguments all pass through a model that has read whatever is in your repository and possibly whatever was on a web page it fetched. Treat the JSON on stdin as untrusted input, because that is exactly what it is, and strip anything you interpolate down to a charset that cannot escape its context.
  • Prefer a script file to an inline command. An inline command in settings.json cannot be reviewed in a diff or tested standalone. A script under .claude/hooks/ can be both.
  • Be careful what a committed project hook does. A hook in .claude/settings.json runs on the machine of everyone who checks out the branch. That is a supply chain, and pull requests that touch it deserve the attention you would give a CI config change.
The good news is symmetrical: hooks are also the best place to enforce anything you have decided about agent behaviour, precisely because they are outside the model and cannot be talked out of it. A limit written into a prompt is a suggestion. The same limit written into a PreToolUse hook is a limit. That is the argument in setting a token budget applied to a different resource.

the fail-open rule

Every hook path fails open. A broken hook must never block an agent. This is the rule we hold ourselves to, in writing, and it is the one thing on this page we would insist on if you took nothing else.

The reason is structural rather than stylistic. A hook sits in the critical path of an agent's work while being the least tested code in the system: a shell script somebody wrote in five minutes, running on every tool call, on a machine whose state it does not control. Sooner or later it meets a payload it cannot parse, a binary that is not installed, an endpoint that is down or a directory that has moved. If any of those exit non-zero on a blocking event, the agent stops, and not with a useful error: with a refused tool call and a reason nobody wrote.

Our own hook server returns HTTP 204 on every error path, including a bad token, malformed JSON and an oversized body. Failing open is not a fallback branch in it, it is the only branch, and a request that fails authentication is answered exactly like one that succeeds. The same rule shapes the script: every guard ends in || exit 0, anything slow is detached so a dead listener cannot stall a turn, and the branch that produces stdout prints either one complete JSON object or nothing at all, because printing nothing is a valid answer and printing half an object is not.

The three rules it collapses to, applied to any hook you write:

  • Exit 0 when you do not know. A guard that cannot parse the command should allow the command. A guard that blocks whenever it is confused blocks everything the first time its parser meets an unusual payload, and you will diagnose that as the model behaving strangely rather than as your own script.
  • Set a timeout, and make it shorter than your patience. Every hook accepts a timeout in seconds. Anything reaching the network gets a small one. Our status POST gets 5 seconds for a request that is already backgrounded, because we would rather lose a status update than delay a tool call.
  • Print nothing unless you mean it. Stdout on exit 0 is a control channel. A stray echo left in from debugging is a malformed decision object, and the CLI is within its rights to do something surprising with it.

There is one deliberate exception, and it is the point of the whole mechanism: a PreToolUse guard that has successfully identified a command it refuses should exit 2. That is not a failure, that is the hook working. Fail open applies to the hook not knowing, never to the hook knowing and deciding.

common questions

What are Claude Code hooks?

Claude Code hooks are shell commands the CLI runs automatically at named points in a session, such as before a tool call, after a tool call, when you submit a prompt, and when the agent stops. Each hook receives a JSON object describing the event on stdin, and its exit code and stdout can allow, block or annotate what happens next. They are configured in a settings.json file and run outside the model, which is what makes them deterministic where a prompt instruction is not.

Where do I put Claude Code hooks?

In the hooks key of a settings.json file. ~/.claude/settings.json applies to every project on your machine, .claude/settings.json in a repository applies to that project and can be committed, and .claude/settings.local.json is the per-machine, gitignored variant. Managed organisation settings and plugins can supply hooks too. Run /hooks inside a session to see which hooks are actually loaded, which is the only listing that reflects the merge of all of them.

Which exit code blocks a tool call in Claude Code?

Exit code 2. On PreToolUse, a hook exiting 2 blocks the tool call and its stderr is shown to Claude as the reason it did not run. Exit code 0 means success, and any other non-zero code is treated as a non-blocking error: the action proceeds and the message goes only to the debug log. Do not use exit 1 to try to block anything. For more control than an exit code gives you, exit 0 and print a decision object on stdout instead.

Can a Claude Code hook stop a dangerous command?

Yes, with a PreToolUse hook matched on Bash that inspects tool_input.command and exits 2 when it matches a pattern you refuse to allow. Treat it as a guardrail against accidents rather than a security boundary, because it is pattern matching on a string and the same command can usually be written another way. It reliably catches the careless case, which is the case that actually happens. Pair it with isolation, so that the worst a stopped command could have done was limited anyway.

Why is my Claude Code hook not firing?

The usual causes, in order of frequency: a settings file that is not valid JSON, an event name spelled wrong, a matcher that does not match the tool name, a script that is not executable, or a session started before the settings change. Run /hooks to see what the session actually loaded, then claude --debug to watch each hook spawn and report its exit code. An unrecognised event name is ignored silently rather than reported as an error, so a typo produces no message anywhere.

Are Claude Code hooks safe?

A hook is arbitrary code running with your shell privileges, triggered by an agent, with no confirmation prompt. It is exactly as safe as the script you wrote and the settings file you accepted. Never accept a settings.json from an untrusted source, quote every variable you interpolate from the payload, and read any hook that a template or a plugin installs on your behalf. A hooks block committed to a shared repository runs on every teammate's machine, so review changes to it the way you review CI configuration.

one implementation

We build a macOS app called Skribbl, and its agent status comes from this hook stream rather than from scraping terminal output, which is why the rules on this page are ones we hold ourselves to rather than ones we read. The managed script it installs is merged into your ~/.claude/settings.json alongside your own hooks rather than replacing them, every path in it exits 0, and the loopback server it POSTs to answers 204 on every error because a hook that can wedge an agent is worse than no hook. The event list in the section above is the one we install, and each entry in it was probed against a real CLI before it shipped.

None of that requires the app. Every example on this page is a settings file and a shell script and works with nothing installed but Claude Code. If you want the shape of what we built on top of it, the docs describe it, the download page has the build, and the comparison page is honest about the other tools in the space. If you are still choosing a CLI, the hook seam is a real differentiator and we compared the two we know best in Claude Code vs Codex CLI.

The next thing worth building with a hook, once you have a formatter and a guard, is a reviewer: a second model reading the diff the first one produced. That has its own post, because the interesting part is not the wiring, it is the prompt.

READ NEXT
Using a second agent to review the first one has writtenWhy a cold second model catches what the author cannot, and what it still misses.9 minClaude Code vs Codex CLI, on the same repositoryApproval models, hookability and what each one shows you while it works.10 minWho may command whom: permissions for coding agentsTwo permission questions. Most setups only answer the first one.9 min
ON THIS PAGE
what a hook iswhere hooks are configuredthe events, one by onethe JSON on stdinexit codes, and which blocksthree worked exampleswhen a hook does not firea hook is arbitrary codethe fail-open rulecommon questionsone implementation
run them on a canvasSkribbl puts every agent, its terminal and what it is spending on one board. macOS, one day free.

get the next one by email.

One email when there is something worth reading. Unsubscribe is one click and it is in every issue.

get me
productpricingdocsquestionswhat it iscomparereleaseswritingnewsletterlaunchesprivacycancel
give them infinity.© skribbl