At 03:03Z a permission prompt for a plain ls reached a phone over remote control. The role that ran it is locked down, and there is no human in its session to answer a prompt.

The role has a narrow allow list and a big deny list. The point of that setup is that anything outside the allow list gets refused silently. On Claude Code 2.1.282, a background subagent’s tool call that reaches this fork does not get auto-denied. The harness surfaces the permission prompt over remote control instead. The ls was on neither list, so the phone got an approval request.

Finding the hook that fires first

A permission-request hook exists. The changelog describes it as a way to automatically approve or deny tool permission requests with custom logic. A call only reaches it when neither the allow list nor the deny list has resolved the call. For a lockdown role, the only sane answer at that point is deny, every time.

Reading the answer out of the binary

The open question was the shape of the hook’s answer. The general hook documentation describes the output of a different hook, the one that fires before a tool runs, and its permission field is explicitly specific to that hook. Neither the changelog nor the embedded hook docs said what this hook’s own JSON should contain.

The installed CLI is a single bundled executable with the source strings still readable inside it, so we read its bundled JS. A grep for the case"PermissionRequest" branch found the hook-output parser. It reads a decision object off the hook’s JSON. It treats the behavior as allow only if it is exactly "allow", and anything else, a typo included, counts as deny. The confirmed shape:

{"hookSpecificOutput": {"hookEventName": "PermissionRequest",
"decision": {"behavior": "deny", "message": "..."}}}

That shape comes from the bundled code of the version that actually runs it, not from the documentation.

A hook with no judgment in it

With the schema settled, the hook is deliberately dumb. It logs one line and answers every PermissionRequest with deny and a message. There is no classification and there are no cases. The real boundary is still the role’s allow and deny lists. The hook only makes sure that “on neither list” resolves to deny instead of a phone prompt, which is what the locked-down mode already promises everywhere else.

We tested it against a throwaway role directory and a real background subagent rather than a simulated one. The hook fired, denied with the custom message, logged one line, and returned immediately with no hang.

A call that is on neither list should never wait for a human who isn’t there. That only holds if the code enforces it, so undecided has to mean no.