How Craft Agents Permission Modes Work: Safe (Explore), Ask (Ask to Edit), and Allow-All (Execute)

Craft Agents uses three permission modes—safe (Explore), ask (Ask to Edit), and allow-all (Execute)—to control whether tools run automatically, require user approval, or are blocked entirely, with each session maintaining its own isolated ModeState.

The craft-ai-agents/craft-agents-oss repository implements a per-session permission system that governs how AI agents interact with filesystem and shell tools. Every conversation runs inside a session that owns a dedicated ModeState object, determining whether write operations proceed silently, prompt for confirmation, or are denied completely.

The Three Permission Modes Explained

The runtime stores three internal keys (safe, ask, allow-all) defined in packages/shared/src/agent/mode-types.ts, while the UI and configuration files use human-readable canonical names.

Safe Mode (Explore)

Safe mode provides a read-only sandbox where all write-type tools—including Write, Edit, MultiEdit, and NotebookEdit—are blocked. Bash commands are filtered against an allow-list defined in ~/.craft-agent/permissions/default.json. The function getBashRejectionReason in packages/shared/src/agent/mode-manager.ts analyzes each command and returns either null (allowed) or a detailed BashRejectionReason explaining the block. No user prompts are shown; rejected commands simply fail with a message directing the user to switch modes.

Ask Mode (Ask to Edit)

Ask mode is the default interactive setting. When the agent attempts a dangerous operation blocked by safe-mode rules, the system creates a PermissionRequest event (type: 'permission_request' in the RPC protocol) and pauses execution until the user responds. The chosen mode is then stored back via setPermissionMode. This mode balances automation with safety by requiring explicit confirmation for writes while allowing reads to proceed unchecked.

Allow-All Mode (Execute)

Allow-all mode skips all permission checks, permitting every tool to run immediately. According to the source code in packages/shared/src/agent/mode-manager.ts, the manager bypasses getBashRejectionReason entirely and sends commands directly to the tool implementation. This mode is only advisable for fully trusted scripts or isolated environments where security constraints are unnecessary.

Where Permission Mode State Lives

Each session maintains its own ModeState managed by the singleton ModeManager exported from packages/shared/src/agent/mode-manager.ts.

The ModeState object contains:

  • permissionMode – the current mode (safe, ask, or allow-all)
  • previousPermissionMode – the mode before the latest transition (used for diagnostics)
  • modeVersion – a monotonic counter incrementing on every change, enabling UI components to detect updates
  • lastChangedBy – the actor who triggered the change (user, system, restore, etc.)

The ModeManager keeps a Map<string, ModeState> keyed by session ID, guaranteeing isolation between concurrent sessions. When a new workspace is created, the optional defaults.permissionMode field in WorkspaceConfig (defined in packages/shared/src/workspaces/types.ts) specifies the inherited mode; if omitted, the system falls back to ask.

How Modes Gate Tool Execution

Safe Mode Validation

In safe mode, the hard-coded set SAFE_MODE_CONFIG.blockedTools prevents any write operations. For Bash commands, getBashRejectionReason validates against allowedBashPatterns from the permissions JSON. The engine uses incremental regex diagnostics to pinpoint mismatches and suggest actionable fixes (for example, "run from within the repo directory").

Ask Mode Permission Requests

When a command is rejected by safe-mode checks while in ask mode, the agent emits a PermissionRequest event. The UI displays the request, and the user's response may temporarily switch to allow-all or maintain ask mode via setPermissionMode(sessionId, mode, metadata).

Allow-All Bypass

In allow-all mode, the permission layer is completely bypassed. The command proceeds directly to the tool implementation without invoking getBashRejectionReason or checking blocked tool lists.

Changing Permission Modes

Programmatic API

The mode-manager.ts module exports setPermissionMode(sessionId, mode, metadata?) to update state programmatically. It returns true when the mode actually changes and fires callbacks to subscribers.

import { setPermissionMode, getPermissionMode } from '@craft-agent/shared/agent/mode-manager';

// Switch a session to Execute (allow-all)
setPermissionMode(sessionId, 'allow-all', { changedBy: 'user' });

UI Shortcut

Users can press SHIFT + TAB in the interface to invoke cyclePermissionMode(sessionId, enabledModes?). This function walks through the ordered list PERMISSION_MODE_ORDER = ['safe', 'ask', 'allow-all'] defined in packages/shared/src/agent/mode-types.ts and commits the next mode with changedBy: 'user'.

Persistence

When a session restores after a restart, hydratePreviousPermissionMode recovers the previousPermissionMode value without modifying the current mode, preserving transition history across launches.

Configuration and Bash Allow-Lists

The permissions configuration resides in ~/.craft-agent/permissions/default.json and follows the schema defined in packages/shared/src/agent/mode-types.ts:

{
  "allowedBashPatterns": ["...regex…"],
  "allowedWritePaths": ["...glob…"],
  "blockedCommandHints": [
    { "command": "git", "reason": "...", "example": "git status" }
  ]
}

These patterns compile at runtime into CompiledBashPattern objects. The PermissionPaths type in mode-types.ts enables error messages to link directly to the configuration file requiring edits.

Code Examples

Query the Current Mode

import { getPermissionMode } from '@craft-agent/shared/agent/mode-manager';

const cur = getPermissionMode(sessionId);
console.log(`Session ${sessionId} is in ${cur} mode`);

Switch to Ask Mode

import { setPermissionMode } from '@craft-agent/shared/agent/mode-manager';

setPermissionMode(sessionId, 'ask', { changedBy: 'user' });

Cycle Through Modes

import { cyclePermissionMode } from '@craft-agent/shared/agent/mode-manager';

const newMode = cyclePermissionMode(sessionId); // → next in SAFE → ASK → ALLOW-ALL
console.log(`Mode changed to ${newMode}`);

Handle Rejected Bash Commands

import { getBashRejectionReason, formatBashRejectionMessage } from '@craft-agent/shared/agent/mode-manager';
import { PERMISSION_MODE_CONFIG } from '@craft-agent/shared/agent/mode-types';

function tryCommand(cmd: string, sessionId: string) {
  const mode = getPermissionMode(sessionId);
  if (mode !== 'allow-all') {
    const reason = getBashRejectionReason(cmd, /* config resolved from permissions */);
    if (reason) {
      console.log(formatBashRejectionMessage(reason, PERMISSION_MODE_CONFIG[mode]));
      // UI would now show a permission request to the user.
      return;
    }
  }
  // Execute command safely…
}

Summary

  • Three modes govern tool execution: safe (read-only), ask (prompt before edit), and allow-all (unrestricted execution).
  • ModeState is stored per-session in a ModeManager singleton, keyed by session ID to ensure isolation.
  • Default behavior falls back to ask mode unless overridden by WorkspaceConfig.defaults.permissionMode.
  • Safe mode blocks write tools and validates Bash commands against ~/.craft-agent/permissions/default.json using getBashRejectionReason.
  • Mode transitions occur via setPermissionMode, UI shortcuts (cyclePermissionMode), or persistence hooks (hydratePreviousPermissionMode).

Frequently Asked Questions

What is the default permission mode for new Craft Agents sessions?

New sessions default to ask (Ask to Edit) mode unless the workspace configuration specifies otherwise. The WorkspaceConfig interface in packages/shared/src/workspaces/types.ts allows setting defaults.permissionMode to safe, ask, or allow-all, which newly created sessions inherit upon initialization.

How does safe mode determine which Bash commands are allowed?

Safe mode uses the getBashRejectionReason function in packages/shared/src/agent/mode-manager.ts to check commands against regex patterns defined in allowedBashPatterns within ~/.craft-agent/permissions/default.json. Commands matching the allow-list return null and proceed; others return a BashRejectionReason detailing why the command was blocked.

Can I change permission modes programmatically during a conversation?

Yes. Import setPermissionMode from @craft-agent/shared/agent/mode-manager and call it with the session ID and desired mode (safe, ask, or allow-all). The function updates the ModeState, increments the modeVersion counter, and notifies subscribers, returning true if the mode actually changed.

What happens to permission mode when I restart the application?

The current mode persists, but the previousPermissionMode field is restored separately via hydratePreviousPermissionMode. This ensures that diagnostic history survives restarts while maintaining the active mode, allowing the system to track who changed the mode and when across application launches.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →