context-attachment-manager
Claude Desktop용 플러그인, 다음 메시지에 사용할 과거 이미지 선택 가능
Claude가 초안을 만들면, 전송 여부는 사용자가 결정, 승인 전까지 전송·게시·결제를 차단
Claude can draft it. You decide if it sends. A tested hook that blocks sends, posts and payments until you approve them.
플러그인JavaScriptGitHub @Kylefrc온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Claude Code PreToolUse hook that blocks sends, posts and payments until you approve the exact action. Installs as a plugin. Dependency-free Node, MIT, 156 tests.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A Claude Code PreToolUse hook that blocks tool calls which send a message, publish a post, or move money, unless you approved that specific action a moment ago.
Dependency-free Node (20 or newer), MIT licensed, 156 regression tests. Independent project, not affiliated with or endorsed by Anthropic.
A rule in a prompt or in CLAUDE.md ("always ask before sending") is advice. The model usually follows it. Usually is not a control.
The Claude Code permissions docs say it directly: instructions in your prompt or CLAUDE.md "shape what Claude tries to do, but they don't change what Claude Code allows." If you run in a mode that skips permission prompts (bypassPermissions is documented as one), or you have clicked "always allow" on a mail or chat tool, nothing outside the model's own judgment sits between a draft and a send.
A hook does not depend on the model's judgment. Hooks run before the permission prompt, and a hook that returns a deny decision, or exits with code 2, stops the tool call. So the check lives in code that the model cannot talk its way past, and the approval step is something you do on purpose.
This repo is that check, for the narrow case of "this action reaches a person or moves money".
References:
"deny" prevents the tool call; exit code 2 blocks it)What was checked: on 2026-10-25, Claude Code 2.1.310 was run headless (claude -p) with --dangerously-skip-permissions and this hook wired in. A gated sendmail -bp (it only prints the mail queue) was denied; the approve script, run from Claude's Bash tool with its file name split so the text guard did not match, exited 1 and wrote nothing; and after an approval typed at a terminal, the gated command went through once. The transcript is LIVE-TEST.txt. That was headless mode, not the interactive screen, one version, one machine. The test suite runs the hook directly with sample payloads. Run the "Try it" step below in your own setup before you rely on it.
Claude Code sends the hook a JSON payload on stdin (tool_name, tool_input) before every tool call. The gate classifies the call. If it does not look like a send or a payment, the hook exits 0 and prints nothing. If it does, the policy is:
~/.claude/away.flag exists, deny. Nothing gets out, approved or not.--match, it only fits a call that mentions that text.Before any of that, a guard denies a call that would approve a send, lift away mode, uninstall the gate, edit the gate's own files or build a pseudo-terminal.
What counts as a send:
send_email, chatPostMessage). It is gated when a send, pay, buy or notify word appears (send, reply, forward, publish, tweet, post, submit, pay, charge, refund, transfer, payout, sms, dial, order, broadcast, invoice, finalize, void, cancel, comment, merge, schedule, book, notify, invite, enroll, buy, purchase, checkout) and no read word (list, get, read, search, find, fetch, query, ...) comes before it. send_email, create_refund, finalize_invoice and merge_pull_request are blocked. create_draft, list_sent_messages, list_invoices and get_order pass. Drafts save; they do not send.mail, sendmail, mutt, msmtp), osascript scripts that send from Mail, Messages, Outlook or Slack, gh issue|pr|release writes and gh api writes, aws ses|sns send/publish, the stripe CLI's create/cancel/refund verbs, and curl, wget, python, node and similar that make a mutating HTTP call (POST, PUT, PATCH, DELETE, -d, --data, --post-data, requests.post) to a known mail, chat, SMS, CRM, accounting, scheduling or payment host, or call a send SDK (smtplib, Twilio messages.create). That includes calls hidden inside a script file the command runs. The program is what counts, so git commit -m "fix sendmail handling" and grep "curl -X POST https://api.stripe.com" docs/ pass.The token is a file, ~/.claude/state/send-gate/send-approved. You mint it, in your own terminal window, not through Claude:
node ~/.claude/bin/approve-send.mjs --match [email protected] "invoice reminder"
The script prints what you are approving, shows a random 4-digit code, and writes the token only if you type that code. It reads the code from /dev/tty, the terminal a person is sitting at. Claude's Bash tool has no terminal, so there the script prints an error, exits 1 and writes nothing, and piping a code into it does not help. --match is optional; with it, the token only fits a call whose tool name or input contains that text (case-insensitive), and a different call is denied without using the token up.
The token is valid for 180 seconds by default (tokenTtlSeconds in send-gate.json changes that). A stale token is deleted and the call is denied. The note you pass is written to the log so you can audit what each approval was for.
Away mode is one flag file. Turning it on works anywhere; turning it off also asks for a code at your own terminal:
node ~/.claude/bin/away-mode.mjs on # block every gated action, token or not
node ~/.claude/bin/away-mode.mjs off # needs a person at a terminal
node ~/.claude/bin/away-mode.mjs status
Every decision is appended to ~/.claude/state/send-gate/send-gate.log.
This is real output from the shipped files, run against a throwaway HOME the same way the tests run them (the terminal step was driven through a pseudo-terminal, standing in for you). The deny message is one JSON line in practice; it is wrapped here to read it.
1. send with no token
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"SEND GATE: this action reaches a person or moves money (mcp__gmail__send_email). It was NOT sent.
To proceed: (1) show the owner the exact final content and recipient and get an explicit \"yes\" to THAT action in this conversation; (2) ask the owner to approve it in their own terminal window, not through you: node ~/.claude/bin/approve-send.mjs --match \"<recipient>\" \"<what>\" (it asks them to type a code, which you cannot do); (3) when the owner says it is approved, retry the same call within 180s.
Do not run approve-send yourself and do not try to create the approval another way. Unattended or scheduled runs stay draft-only."}}
2. you, in your own terminal: node bin/approve-send.mjs --match [email protected] "invoice reminder"
SEND GATE approval. This lets ONE gated action through, once, in the next few minutes.
Action: invoice reminder
Only for a call that mentions: [email protected]
Type 5601 and press Return to approve (anything else cancels): 5601
Approved the next gated action (invoice reminder; only calls mentioning "[email protected]"). Tell Claude it is approved; it expires soon.
3. same send again -> no output, exit 0 (allowed, token consumed)
4. same send a second time -> deny
5. mcp__gmail__create_draft -> no output, exit 0 (allowed)
6. away mode on, new token, same send -> deny ("away mode is ON ...")
and the log from that run (message text shortened):
DENY mcp__gmail__send_email :: SEND GATE: this action reaches a person or moves money ...
ALLOW mcp__gmail__send_email :: token "invoice reminder" (match "[email protected]") (0s old)
DENY mcp__gmail__send_email :: SEND GATE: this action reaches a person or moves money ...
DENY mcp__gmail__send_email :: SEND GATE: away mode is ON, so nothing that reaches a person or moves money can run. ...
If you would rather not run the installer, add it from the plugin marketplace in this repo. Inside Claude Code:
/plugin marketplace add Kylefrc/claude-code-send-gate
/plugin install send-gate@send-gate
(On Claude Code 2.1.275 or later, /plugin install send-gate --marketplace Kylefrc/claude-code-send-gate does both steps.) Then run /hooks to confirm the PreToolUse entry is listed; start a new session if it is not. The hook runs from the plugin's own folder, so nothing is written to your settings.json.
The approval script is not copied to ~/.claude/bin/ by the plugin, but the deny message points there. Copy the two scripts once, from the plugin folder, so the printed command works:
mkdir -p ~/.claude/bin && cp ~/.claude/plugins/cache/send-gate/send-gate/*/bin/*.mjs ~/.claude/bin/
Use the plugin or install-gate.mjs, not both. The approval token is single-use, so with two copies of the hook the first one consumes it and the second denies the call. To remove the plugin: /plugin uninstall send-gate@send-gate. The same Node 20+ and macOS or Linux notes as below apply, and the plugin route has been tested only through claude plugin validate and a local install, not on Windows.
Needs Node 20 or newer (node -v) and Claude Code on macOS or Linux. On Windows, use WSL: the hook command in settings.json is a POSIX shell line.
git clone https://github.com/Kylefrc/claude-code-send-gate && cd claude-code-send-gate
node install-gate.mjs --dry-run
node install-gate.mjs && node tests/test-send-gate.mjs
The installer:
hooks/send-gate.mjs to ~/.claude/hooks/, and bin/approve-send.mjs and bin/away-mode.mjs to ~/.claude/bin/--force~/.claude/settings.json to settings.json.bak-send-gate before changing it, adds one PreToolUse entry (no matcher, so it sees every tool call, 5 second timeout), and leaves your other hooks and settings alonesettings.json is not valid JSONThe wired command fails closed on one specific thing: if node cannot be found, the call is blocked with a message instead of passing silently.
Then run /hooks in Claude Code to confirm the hook is listed (start a new session if it is not).
Try it: ask Claude to send a test message through any connected mail or chat tool. It should be blocked and told how to ask you for approval. Approve it the proper way (a second terminal window of your own, node ~/.claude/bin/approve-send.mjs --match "<recipient>" "<what>", type the code), tell Claude it is approved, and watch it go through once.
$ node tests/test-send-gate.mjs
156 passed, 0 failed
Run on macOS (Darwin 24.1.0) with Node v20.11.0 and v22.23.1. The 156 cases are 89 classification checks (what is gated and what passes, including commands that only mention a sender), 6 token checks (one use, consumed, untouched by ungated calls, stale tokens denied and removed), 6 recipient-match checks, 3 away-mode checks, 31 self-approval guard checks, 8 config checks and 13 terminal checks (the approval scripts refuse without a terminal and work with one). Each case runs in a temporary HOME, so the suite never touches your real ~/.claude. The terminal checks use python3 to drive a pseudo-terminal; without it they are reported as skipped.
What the tests are: the hook run directly with sample JSON payloads. What they are not: a Claude Code session, or proof that every tool in your setup is covered. Passing means those named cases behave as written. After you install, ask Claude to attempt a send with each of your own tools and watch what happens.
TEST-TRANSCRIPT.txt has the transcript of the installer checks (dry run, install, repeat install, uninstall, existing hooks preserved, no overwrite without --force, invalid settings left alone). LIVE-TEST.txt has the headless Claude Code run described above.
Catches, out of the box:
osascript sends, gh issue, PR and release writes, aws ses and aws sns, the stripe CLI, and curl, wget, python or node calls that POST, PUT, PATCH or DELETE to Gmail, Microsoft Graph, SendGrid, Mailgun, Postmark, Resend, Twilio, Slack, Discord, Telegram, Stripe, Square, PayPal, Mailchimp, OpenPhone, Dialpad, HubSpot, Salesforce, Pipedrive, QuickBooks, Xero, Calendly and several more (the full list is HOSTS in the hook)Cannot see, so cover these in your CLAUDE.md and your own review:
extraHosts.notify_customer is listed, but alert_customer is not). Add them to extraGatedTools.git push, npm publish and deploys. Off by default; add "extraShellPatterns": ["\\bgit\\s+push\\b"] to turn on the one you want. GitHub create_issue and similar MCP writes are not gated either; add them to extraGatedTools.submit_review or a spreadsheet's merge_cells, for example). Use allowTools for those.Limits of the design:
--match, the approval is not tied to a particular call: the next gated call within the time window uses the token, whatever its recipient. With --match, the call must mention the text you gave./dev/tty. Claude's Bash tool has no terminal, so Claude cannot type the code. But any process that can run Python can open its own pseudo-terminal and type it (the test suite does exactly that to test the script). The hook therefore also denies commands, and scripts they run, that name approve-send, the token file, away.flag or the gate's own files, or that build a pseudo-terminal. That is text matching, and it can be dodged: in the headless run a split file name got past it (the terminal requirement still stopped the script). I did not try anything cleverer than that. This is a safeguard against mistakes, drift and over-eager automation, not a sandbox.disableAllHooks set outside Claude Code, or starting Claude Code with --bare) turn this off too, and so does an edit to settings.json made outside Claude Code. The hook does block Claude's own Edit, Write and shell writes to settings.json, but a command that builds the file name from pieces can still dodge that text match. A permissions.ask rule on Bash(*approve-send*) adds a second layer; the permission-modes docs say ask rules still prompt in bypass mode, and a deny rule and an ask rule held in a headless bypass run (see LIVE-TEST.txt).Everything is optional. Create ~/.claude/send-gate.json; it is merged over the defaults, and a file that does not parse is ignored.
{
"extraGatedTools": ["^mcp__mycrm__update_deal$"],
"allowTools": ["^mcp__slack__send_message_to_self$"],
"extraHosts": ["api.mycrm.com"],
"extraShellPatterns": ["\\bgit\\s+push\\b"],
"tokenTtlSeconds": 180
}
extraGatedTools: regexes (case-insensitive) matched against the full tool name. A match is always gated.allowTools: regexes for MCP tools to exempt from the name rule. Checked first. It does not exempt shell commands or browser scripts, which have their own checks.extraHosts: hostnames added to the list of mail, chat, CRM and payment hosts for shell commands.extraShellPatterns: regexes tested against each part of a shell command with its quoted text removed. A match is always gated.tokenTtlSeconds: how long an approval stays valid.node install-gate.mjs --uninstall
This removes the PreToolUse entry that references send-gate.mjs (other hooks stay) and the three runtime files. A file that differs from the copy in this repo is left alone unless you add --force. It leaves ~/.claude/send-gate.json, the log in ~/.claude/state/send-gate/, and the settings backup in place. Or restore settings.json.bak-send-gate by hand.
hooks/send-gate.mjs the hook
bin/approve-send.mjs mints a single-use approval token (needs a code typed at a terminal)
bin/away-mode.mjs on / off / status for the away flag (off needs a terminal)
tests/test-send-gate.mjs 156 cases, runs in a temporary HOME
install-gate.mjs installer / uninstaller
.claude-plugin/plugin.json plugin manifest
.claude-plugin/marketplace.json marketplace entry (source is this repo)
hooks/hooks.json plugin hook wiring (runs hooks/send-gate.mjs)
TEST-TRANSCRIPT.txt installer test transcript
LIVE-TEST.txt headless Claude Code run, bypass mode
I also wrote a fuller Claude Code setup (a CLAUDE.md template, a memory system, two skills, rules for sub-agents, seven rule packs for common business tools and a written playbook) that is sold separately as The Operator Kit for Claude Code, USD 19 at https://operatorkit-cc.netlify.app. The gate here is complete without it.
MIT. See LICENSE.