How nodeterm Suppresses Grok's Routine Per-Tool Permission Prompts
TLDR: nodeterm suppresses Grok's routine per-tool permission prompts by canonicalising Grok's camel-case hook payloads, denylisting the permissionprompt notification type in src/shared/agents/normalize.ts, and routing Grok's idle notifications through the same permission-mode flag handling used by Claude.
According to the eneskirca/nodeterm source code, Grok is treated as a built-in agent with a dedicated normalization path. When Grok emits a notification that would normally trigger a "permission required" UI prompt, nodeterm classifies it as a routine idle message rather than a blocked-agent event. This keeps the interface clean and avoids unnecessary user interruptions.
How nodeterm's Grok Permission Prompt Suppression Works
The suppression mechanic is implemented in three coordinated layers across src/shared/agents/normalize.ts and src/shared/agents/approval-mode.ts. Each layer handles a distinct part of the problem: identifying the agent, filtering the notification type, and ensuring permission-mode flags don't accidentally re-trigger the blocked state.
1. Canonicalisation of Grok's Hook Payload
Grok's hook payload uses camel-case keys that differ from every other agent supported by nodeterm. To compare payloads reliably, normalize.ts defines two helper functions:
const grokCanonical = (v?: string) => (v ?? '')
.toLowerCase()
.replace(/[^a-z]/g, '');
const grokEventName = (p: GrokPayload) =>
grokCanonical(p.hookEventName ?? p.hook_event_name);
const notificationType = grokCanonical(p.notificationType ?? p.notification_type ?? p.type);
grokCanonical lower-cases the input and strips every non-alphabetic character, producing a stable identifier. grokEventName and notificationType then use that canonical form to recognize the event name and notification type regardless of whether Grok sent camel-case or snake-case keys.
2. Filtering Routine Notifications with a Denylist
Inside grokRawFields, after extracting the event and type, nodeterm checks the canonicalised type against a denylist. The value permissionprompt — which is Grok's specific wording for a per-tool permission request — is recognized and silently ignored:
// The asking types are a CLOSED set; grok's routine permission_prompt is excluded.
if (type === 'permissionprompt') {
// Suppress the prompt – treat as idle.
return null;
}
Returning null prevents the generic "blocked" badge from appearing on the node and stops the UI from prompting the user. Because the routine prompt is filtered at this stage, it never becomes a blocked-agent event.
3. Unified Permission-Mode Handling
Grok shares the same flag syntax as Claude (--permission-mode). The generic permission-mode utilities in src/shared/agents/approval-mode.ts — hasPermissionMode and withPermissionMode — explicitly include Grok in their capability list:
const PERMISSION_MODE_CAPABLE = ['claude', 'grok'];
When a Grok node is created, withPermissionMode('grok', …) appends the appropriate flag only for modes other than the default manual. Since the routine prompt is already filtered in the normalization step, Grok never reaches the "blocked" state even if the user has selected a non-default mode.
Practical Code Examples
Creating a Grok node with the correct permission flag
import { createAgentNode } from 'src/renderer/state/workspace';
import { withPermissionMode } from 'src/shared/agents/approval-mode';
// Example: start a Grok session with "auto" mode (the default for Grok)
const cmd = withPermissionMode('grok', 'grok', 'auto'); // -> "grok --permission-mode auto"
createAgentNode('grok', { initialCommand: cmd });
Normalizing a Grok hook payload
import { grokRawFields } from 'src/shared/agents/normalize';
const payload = {
hookEventName: 'preToolUse',
notificationType: 'permissionPrompt',
// ...other fields
};
const result = grokRawFields(payload);
// result is null because the notification type is the routine permission prompt.
Suppressing the UI badge in the status reducer
// In the agent-status store reducer
if (normalized === null) {
// Grok routine prompt => ignore, keep current state.
return state;
}
Because the normalization step returns null, the reducer keeps the node in its current state and never marks it as blocked.
Key Files in the nodeterm Repository
| File | Purpose |
|---|---|
src/shared/agents/normalize.ts |
Core Grok payload parsing and suppression logic |
src/shared/agents/approval-mode.ts |
Permission-mode utilities that include Grok |
src/shared/agents/normalize.grok.test.ts |
Test suite confirming the suppression behavior |
src/shared/keybindings.ts |
Registration of the "New Grok node" command |
These files together implement the detection, canonicalisation, and filtering that keep Grok's routine per-tool permission prompts from surfacing in the Nodeterm UI.
Summary
- Grok prompts are suppressed at the normalization layer:
grokRawFieldsreturnsnullfor thepermissionpromptnotification type, preventing any blocked state. - Canonicalisation handles Grok's unique key format:
grokCanonicallower-cases and strips non-alphabetic characters so event names and notification types can be compared reliably. - Permission-mode handling is unified with Claude:
withPermissionModeincludes Grok, but because the routine prompt never reaches the reducer, the wrong flag never produces a blocked badge. - The behavior is covered by tests:
normalize.grok.test.tsverifies the suppression works as expected.
Frequently Asked Questions
Why does Grok use permissionPrompt instead of permission_prompt?
Grok's hook payload uses camel-case keys, which differ from the snake-case style used by most other agents. nodeterm's grokCanonical normalizes both formats into a single lower-case identifier, so permissionPrompt, permission_prompt, and permissionprompt are all detected as the same routine event.
Does nodeterm suppress all Grok permission prompts, not just routine ones?
No — the suppression only targets the routine permissionprompt notification, which is the notification Grok sends on every ordinary tool use. If Grok emits a genuinely blocking event (such as a different event type not in the denylist), it will still be processed normally and can still trigger a blocked state in the UI.
How does forcing a non-default permission-mode affect Grok's prompts?
The withPermissionMode helper adds the --permission-mode flag to the Grok command for modes other than the default manual. However, because the routine permissionprompt notification is already filtered at the normalization layer, that flag never translates into a visible "blocked" badge in the interface.
Where in the source code can I find the denylist logic?
The denylist check lives inside grokRawFields in src/shared/agents/normalize.ts. The exact line compares the canonicalised notification type against the string permissionprompt and returns null to suppress the prompt.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →