Welcome back! In Lifecycle Hooks, we learned how to set up "security checkpoints" to block dangerous commands like rm -rf /.
However, the world isn't always Black and White (Allow or Block). Sometimes, it's Gray.
This complex decision-making process is called Permission Resolution.
Imagine a popular nightclub (your computer). The Permission Resolver is the Head Bouncer at the door.
When the AI (a guest) wants to enter (run a tool), the Bouncer checks three things in a specific order:
Without this resolution logic, the AI would either be paralyzed (asking permission for everything) or dangerous (doing everything without asking).
In this chapter, we will solve this scenario: We want the AI to read files automatically (for speed), but we want it to ASK explicitly before writing or deleting files (for safety).
The Permission Resolver combines inputs from different sources to create a final PermissionDecision.
settings.json) that says "Always Allow Reading" or "Always Deny Network".The decision always results in one of three behaviors:
allow: Run the tool immediately.deny: Stop. Throw an error.ask: Pause execution and show a dialog to the user.
The logic lives inside toolHooks.ts in a function called resolveHookPermissionDecision. It acts as the mediator between the hooks and the user.
Here is the decision flow:
Let's look at resolveHookPermissionDecision in toolHooks.ts. We will simplify it to understand the core logic.
If a hook (like a trusted script) says "Allow", we usually let it pass. However, we have a safety catch.
// Inside resolveHookPermissionDecision
if (hookPermissionResult?.behavior === 'allow') {
// Even if a hook says "Allow", check if the user
// explicitly configured "Always Deny" in settings.
const ruleCheck = await checkRuleBasedPermissions(tool, input, context);
if (ruleCheck && ruleCheck.behavior === 'deny') {
// Safety Net: User settings override hooks for DENY actions.
return { decision: ruleCheck, input };
}
// Otherwise, trust the hook!
return { decision: hookPermissionResult, input };
}
Explanation: This prevents a rogue plugin from approving an action that you, the user, explicitly banned in your settings. "Deny" rules are the ultimate trump card.
This is simple. If a hook says "No", it's "No".
if (hookPermissionResult?.behavior === 'deny') {
// If a hook blocked it (e.g., security scan), stop here.
return { decision: hookPermissionResult, input };
}
Explanation: This is what allowed us to block rm -rf / in the previous chapter.
If no hooks have a strong opinion (which is most of the time), we defer to the standard permission system (canUseTool).
// If no hooks intervened, use the standard logic
return {
decision: await canUseTool(
tool,
input,
context,
assistantMessage,
toolUseID
),
input // Pass the input along
};
Explanation: canUseTool is the function that actually checks your settings.json or draws the "Approve/Reject" popup on your screen.
Let's trace how our "Read vs. Write" scenario flows through this code.
read_file("data.txt")undefined.canUseTool.settings.json. Finds a rule: read_file: allow.behavior: 'allow'. The tool runs instantly.write_file("data.txt")undefined.canUseTool.settings.json.write_file: deny. -> Result: deny (Error).write_file: ask (or no rule exists). -> Result: ask.behavior: 'allow'.Sometimes, the Bouncer doesn't just say "Yes" or "No"βthey might fix your tie before letting you in.
In the Permission Resolver, a decision can also include updatedInput.
// In resolveHookPermissionDecision
if (hookPermissionResult?.behavior === 'allow') {
// If the hook fixed the input (e.g., corrected a file path)
// we use that new input going forward.
const finalInput = hookPermissionResult.updatedInput ?? input;
return { decision: hookPermissionResult, input: finalInput };
}
Why is this useful?
Imagine the AI tries to read User/docs/file.txt. A hook knows the real path is /Users/docs/file.txt. The hook can allow the action AND fix the path simultaneously, so the tool succeeds without the AI having to retry.
This resolution logic is called right inside the pipeline we built in Chapter 1.
From toolExecution.ts:
// 1. Run Hooks
// ... (code from Chapter 2)
// 2. Resolve Permissions
const resolved = await resolveHookPermissionDecision(
hookPermissionResult, // What did hooks say?
tool,
processedInput,
// ... context ...
);
// 3. Act on Decision
if (resolved.decision.behavior !== 'allow') {
// If it's 'deny' or the user clicked 'Cancel' on the 'ask' dialog
return createRejectionMessage("Permission denied");
}
// 4. Run Tool
// ... tool.call() ...
Permission Resolution is the sophisticated logic that keeps the system safe while remaining usable. It ensures that:
Now that we have permission to run the tool, and we know how to run it... what happens if the tool produces a massive amount of data, or takes a long time to finish? We don't want the UI to freeze!
Next Chapter: Streaming Tool Executor
Generated by Code IQ