SLOPSHOPPER

doorbell-grant

Approves the doorbell plugin's own read-only inbox calls at the permission check, so its background refresh works in auto mode.

newguard
v0.1.0MITupdated 2026-10-12nthplusio/skills/plugins/doorbell-grant
A shopper browsing a rack in a slop shop
README

doorbell-grant

Approves the doorbell plugin's own read-only inbox calls at Claude Code's permission check, so doorbell's inbox view can refresh in the background, including in auto mode. Installing doorbell installs this plugin; you don't install it yourself.

Why it is a separate plugin

Doorbell's inbox view refreshes from a timer, and every refresh is an MCP tool call that meets the permission check. When a plugin raises a call outside one of its own running hooks, Claude Code skips every hook of that plugin for the call. So doorbell cannot approve its own background calls, and in auto mode the classifier denies them.

Another plugin's hooks still run for that call, and the host tells them which plugin raised it (next.origin). That is the only part doorbell-grant relies on. A plugin cannot set its own origin.

What it approves

One tool.check hook answers allow when all of these hold:

  • The host reports doorbell as the caller.
  • The tool is one of doorbell's four read-only inbox tools, under any MCP server name: list_agent_roles, peek_mcp_pending, list_mcp_message_sources, or get_mcp_message_history.
  • The normal check did not deny it.

Every other call goes to the normal permission check unchanged. That includes doorbell's writes (leases, acknowledgments, publishing, binding), any other plugin's calls, and Claude's own calls.

An approval from a plugin's tool.check hook settles the check before your permission mode is asked, so these four calls run without a prompt or a classifier decision. The hook asks the normal check first and keeps any deny it returns, so a deny rule for these tools, yours or your organization's, still blocks them. It only turns a would-be prompt into an approval. Managed settings can also stop the plugin from loading.

Verify

npm run test:doorbell-grant

The tests load stand-in plugins named doorbell and other that ask the permission check about a tool, and check the verdicts.

The script's tsc step needs the API types Claude Code writes into .claude-plugin/types/ when it loads the plugin, and claude plugin test does not write them. In a fresh checkout, run npm run test:plugins: doorbell's connected check loads both plugins before this script runs.

Source 1 files
hooks/register.ts 25 lines
1import type { Register } from 'claude-code'
2
3// Doorbell refreshes its inbox from a timer. A call a plugin raises outside a
4// live dispatch skips every hook of that plugin, so doorbell cannot approve its
5// own background calls, and auto mode's classifier denies them. Another
6// plugin's hooks still run for that call, with next.origin set by the host.
7// This plugin approves only doorbell's read-only inbox tools, only when
8// doorbell raised the call, and never over a deny; every other call goes
9// to the normal check.
10const DOORBELL = 'doorbell'
11const READ_ONLY_TOOLS = new Set(['list_agent_roles', 'peek_mcp_pending', 'list_mcp_message_sources', 'get_mcp_message_history'])
12
13export const register: Register = on => {
14  on('tool.check', async ($, e, next) => {
15    // Any server name: Doorbell may run under a name other than the plugin's.
16    const tool = e.tool.startsWith('mcp__') ? e.tool.slice(e.tool.lastIndexOf('__') + 2) : ''
17    // Not caught: a failure in the normal check must reach the caller unchanged.
18    if (next.origin.plugin !== DOORBELL || !READ_ONLY_TOOLS.has(tool)) return next(e)
19    // The verdict beneath comes before the mode settles an ask, so the
20    // classifier is not consulted here. A deny, such as your own rule, wins.
21    const verdict = await next(e)
22    return verdict.decision === 'deny' ? verdict : { decision: 'allow', reason: 'doorbell-grant: the doorbell plugin reading its own inbox' }
23  })
24}
25