Feature request: PreToolUse hooks need a silent-deny permission decision for non-interactive / dontAsk sessions

Resolved 💬 1 comment Opened May 14, 2026 by fokrzesik-rtbh Closed Jun 12, 2026

Summary

PreToolUse hooks currently have three outcomes: silent allow (exit 0, no output), ask (exit 0 + permissionDecision: "ask" JSON), and hard error (exit 2). There is no way to silently deny a tool call from a hook without either blocking for user input or raising an error. This creates a deadlock in scheduled / autonomous sessions running under dontAsk mode.

---

Use case

Scheduled agents run fully autonomously on a cron or on-demand basis, with no user present. The typical harness for such an agent uses:

{
  "permissions": {
    "defaultMode": "dontAsk",
    "allow": ["mcp__some_tool__read_channel", ...]
  }
}

dontAsk correctly silences permission prompts for tools not in allow — the harness denies them and returns an error message to the model, which can then adapt. This is the right behaviour for unattended runs: fail fast and continue, never block.

Now suppose you want parameter-level access control for a listed tool. Example: read_channel is allowed, but only for a specific set of channel IDs. You write a PreToolUse hook that inspects the channel_id argument:

data = json.load(sys.stdin)
channel_id = data["tool_input"].get("channel_id", "")

if channel_id in ALLOWED_CHANNELS:
    sys.exit(0)  # silent allow

# channel not in allowlist — what now?
print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "ask",
        "permissionDecisionReason": f"Channel '{channel_id}' is not in the allowlist."
    }
}))
sys.exit(0)

The hook returns "ask" because there is no "deny" option. In an interactive session this is fine — the user sees a prompt, can approve the new channel, and the run continues. But in an autonomous scheduled session, "ask" overrides dontAsk and the entire run blocks indefinitely, waiting for a user who is not present.

---

The gap

| Desired outcome | Currently available? |
|---|---|
| Allow the call silently | ✅ exit 0, no output |
| Prompt the user | ✅ exit 0 + permissionDecision: "ask" |
| Hard error (aborts the tool call with an error string) | ✅ exit 2 |
| Silently deny (same behaviour as dontAsk for unlisted tools) | ❌ not available |

Exit 2 is a partial workaround but it produces a confusing error rather than a clean "access denied" message, and signals a hook malfunction rather than a deliberate policy decision.

---

Proposed solution

Add permissionDecision: "deny" as a valid hook output:

print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": f"Channel '{channel_id}' is not in the allowlist."
    }
}))
sys.exit(0)

Behaviour should mirror what dontAsk does for unlisted tools: the model receives a structured "access denied" tool result (including the permissionDecisionReason as context), the session is not blocked, and the model can adapt and continue.

---

Alternative / complementary solution

Respect the session's defaultMode when resolving hook "ask" decisions. If the session is running under dontAsk, treat a hook-returned "ask" as "deny" instead of blocking for user input. This would make the hook's escalation mode context-aware, so the same hook works correctly in both interactive and non-interactive sessions.

---

Impact

Without this, any PreToolUse hook that does parameter-level validation (allowlisting specific resource IDs, file paths, API scopes, etc.) is incompatible with fully autonomous scheduled sessions. The only safe options today are:

  1. Return "ask" and accept that autonomous runs can stall indefinitely.
  2. Return exit 2 and accept noisy errors.
  3. Remove the hook entirely and lose parameter-level access control.

A "deny" decision (or dontAsk-aware "ask" resolution) would make hooks composable with autonomous agents, which is an increasingly common deployment pattern.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗