Understanding the Three Permission Modes in Craft Agents
Craft Agents enforces three permission modes—Explore (safe), Ask to Edit (ask), and Execute (allow-all)—that govern tool access, and you can switch between them programmatically using setPermissionMode() or interactively via cyclePermissionMode() and the Shift+Tab shortcut.
The craft-ai-agents/craft-agents-oss repository implements this permission system in the shared @craft-agent package. These permission modes are deliberately isolated per-session, ensuring that mode switches never leak state between concurrent agent sessions.
The Three Permission Modes
The mapping between internal keys and UI-facing names is defined in packages/shared/src/agent/mode-types.ts at lines 24-30. Each mode controls what tools and commands an agent may execute.
Explore Mode (safe)
Explore Mode is the read-only default. In safe mode, all write-type tools—such as Write and Edit—are blocked, and the agent operates without asking for confirmation. This mode is ideal for investigating codebases or generating explanations without risk of modification.
Ask to Edit Mode (ask)
Ask to Edit Mode enables interactive operation. When set to ask, the agent prompts the user for confirmation before performing any potentially dangerous operation. This balances automation with oversight for sensitive tasks.
Execute Mode (allow-all)
Execute Mode removes all restrictions. Under the allow-all internal key, no permission checks are performed, allowing the agent to write, edit, and execute commands freely. Use this mode only when full automation is required and the environment is secure.
How to Switch Permission Modes
The public API for changing modes resides in packages/shared/src/agent/mode-manager.ts. The system stores per-session state in a Map managed by the singleton modeManager instance (lines 30-33), preventing global mutable state contamination.
Programmatic Mode Assignment
To set a specific mode directly, call setPermissionMode() exported from mode-manager.ts (lines 82-99). This function accepts a sessionId, the target mode ('safe', 'ask', or 'allow-all'), and an options object:
import { setPermissionMode } from '@craft-agent/shared/agent';
const sessionId = 'my-session';
// Switch to Ask to Edit mode
setPermissionMode(sessionId, 'ask', { changedBy: 'user' });
Interactive Mode Cycling
For UI interactions, the cyclePermissionMode function traverses the ordered list PERMISSION_MODE_ORDER—defined as ['safe','ask','allow-all']—and updates the mode for the current session (lines 34-42). The Electron UI binds Shift+Tab to this function, allowing users to toggle modes without code:
import { cyclePermissionMode } from '@craft-agent/shared/agent';
function toggleMode(sessionId: string) {
const newMode = cyclePermissionMode(sessionId);
console.log(`Switched to ${newMode}`);
}
React Integration
The manager emits callbacks via onStateChange and supports React subscribers through useSyncExternalStore (lines 101-108). The ModeState record tracks the current mode, previous mode, a monotonic version counter, and timestamps for debugging (lines 85-99):
import { useSyncExternalStore } from 'react';
import { subscribeModeChanges, getModeState } from '@craft-agent/shared/agent';
function PermissionModeIndicator({ sessionId }: { sessionId: string }) {
const state = useSyncExternalStore(
cb => subscribeModeChanges(sessionId, cb),
() => getModeState(sessionId)
);
return (
<div>
Mode: {state.permissionMode} (v{state.modeVersion})
</div>
);
}
Practical Code Examples
Switching from Explore to Execute via CLI
When automating agent workflows from a command-line script, import setPermissionMode and getPermissionMode to verify transitions:
import { setPermissionMode, getPermissionMode } from '@craft-agent/shared/agent';
async function enableExecute(sessionId: string) {
console.log('Current mode:', getPermissionMode(sessionId)); // safe
setPermissionMode(sessionId, 'allow-all', { changedBy: 'user' });
console.log('New mode:', getPermissionMode(sessionId)); // allow-all
}
Reading Current Mode State
Before initiating tool calls, check the current permission level:
import { getPermissionMode } from '@craft-agent/shared/agent';
const sessionId = 'my-session';
const current = getPermissionMode(sessionId);
// Returns: 'safe' | 'ask' | 'allow-all'
RPC and Server Integration
The same API powers the Electron UI and server-side handlers. The RPC endpoint in apps/electron/src/shared/routes.ts (lines 81-82) and the server implementation in packages/server-core/src/handlers/rpc/sessions.ts (lines 322-323) both delegate to setPermissionMode to honor remote requests.
Summary
- Three permission modes exist: Explore (
safe), Ask to Edit (ask), and Execute (allow-all), defined inmode-types.ts. - State isolation is enforced per-session via a singleton
modeManagerusing aMapstructure, preventing cross-session contamination. - Switching methods include
setPermissionMode()for direct assignment andcyclePermissionMode()for sequential cycling through thePERMISSION_MODE_ORDERarray. - UI integration supports Shift+Tab shortcuts and React subscriptions via
useSyncExternalStorewith immediate callback notifications on state changes. - RPC support allows remote mode changes through the Electron and server-core handlers.
Frequently Asked Questions
What is the default permission mode when starting a new session?
The default mode is Explore (safe), which operates in read-only mode to prevent accidental modifications. You can verify this by calling getPermissionMode(sessionId) immediately after session initialization.
Can permission modes be changed remotely via API calls?
Yes. The server implementation in packages/server-core/src/handlers/rpc/sessions.ts (lines 322-323) exposes RPC endpoints that delegate to setPermissionMode, allowing remote clients to change a session's permission mode securely.
How does the UI know when a permission mode changes?
The modeManager emits callbacks through onStateChange and notifies React subscribers via useSyncExternalStore (lines 101-108 in mode-manager.ts). This ensures the UI updates instantly when modes transition, either programmatically or through the Shift+Tab shortcut.
Is there any global state shared between different sessions?
No. The implementation deliberately isolates state per-session using a Map data structure in the modeManager singleton (lines 30-33). Each session maintains its own ModeState record, ensuring that changing modes in one session never affects another.
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 →