Skip to main content

Overview

The permission system intercepts every tool call an agent makes and produces one of three decisions: allow the tool to execute, deny it, or ask the user for confirmation. The system combines static configuration with dynamic runtime analysis. Three components drive the decision together:
  • Rules — explicit allow/deny/ask patterns per tool and command, evaluated with highest priority. Rules have two sources: statically pre-configured in PermissionContext, or dynamically added when the user accepts a suggested rule during an ASK prompt. Suggestions are auto-generated from the current tool call, so accepting one means future identical calls are handled automatically without prompting again.
  • Mode — a static global policy set at configuration time; determines default behavior for all calls that match no rules (e.g., EXPLORE makes the agent read-only; DONT_ASK silently denies unmatched calls).
  • Built-in checks — dynamic runtime analysis performed by the tools themselves against actual call inputs: read-only command detection (parsing the bash command at call time) and dangerous path protection (checking the real file path or command target). Tools can mark a safety ASK as bypass-immune via PermissionDecision.bypass_immune=True; the engine then honors it across DEFAULT, ACCEPT_EDITS, and DONT_ASK (allow rules cannot silence it). BYPASS mode is the one exception — it explicitly opts out of all safety ASKs by design.
Below is the decision flow for each mode. ASK outcomes trigger user confirmation; if the user accepts the auto-generated suggested rule, it is persisted for future calls.
Deny rules and explicit ask rules are always honored, in every mode (including BYPASS).Tool-emitted safety ASKs (bypass_immune=True) are honored in DEFAULT, ACCEPT_EDITS, and DONT_ASK — they cannot be silenced by allow rules. In BYPASS mode they are skipped on purpose: BYPASS’s contract is “the user has opted out of safety prompts; only deny/ask rules remain as guardrails.”

Permission Mode

AgentScope supports the following modes, each suited to a different deployment scenario.
ModeBehaviorUse Case
DEFAULTAll ops require explicit rules or user confirmation. The only built-in auto-allow is Bash recognizing read-only commands (ls, git status, cat, …) and returning ALLOW. Read / Glob / Grep return PASSTHROUGH and still ASK unless an allow rule matchesMost secure, recommended default
ACCEPT_EDITSAuto-allow file ops in working directories; auto-allow Bash filesystem commands (mkdir/touch/rm/cp/mv/sed) only when every target path resolves inside a working directoryActive development with user present
EXPLORERead-only: allow read-only tools and read-only Bash commands (ls, git status, cat, …); deny modifications. User-configured DENY/ASK rules take precedence over the read-only auto-allowCode exploration, planning
BYPASSSkip all permission checks except deny/ask rules and tool DENY. Tool safety ASKs are NOT enforced (rm -rf /, writes to ~/.bashrc, command injection, etc. all pass through). Use deny rules to protect specific pathsSandboxed environments or fully trusted runs
DONT_ASKConvert every ASK (default, ASK rules, and safety ASKs) to DENY; safe-by-default for non-interactive runsUnattended / scheduled execution
Set the mode via AgentState.permission_context when creating the agent, or update it at runtime.
from agentscope.agent import Agent
from agentscope.state import AgentState
from agentscope.permission import PermissionContext, PermissionMode

agent = Agent(
    name="my_agent",
    system_prompt="...",
    model=model,
    state=AgentState(
        permission_context=PermissionContext(
            mode=PermissionMode.DEFAULT,
        )
    ),
)

Permission Rules

A PermissionRule maps a specific tool and call pattern to one of three behaviors: ALLOW, DENY, or ASK. Each rule consists of the following fields. When the permission engine evaluates a rule, it calls the tool’s match_rule() method with rule_content and the actual call input to determine whether the rule applies.
tool_name
str
required
Tool this rule applies to: "Bash", "Read", "Write", "Edit", or any custom tool name.
rule_content
str | None
required
Match pattern — semantics depend on tool_name:
  • Bash: wildcard prefix pattern (npm run:* matches npm run build, npm run test)
  • Read / Write / Edit: glob pattern (src/**/*.py matches any .py under src/)
  • Other tools: exact JSON-serialized parameter match
behavior
PermissionBehavior
required
ALLOW, DENY, or ASK
source
str
required
Origin of the rule: "userSettings", "projectSettings", "session", etc.

Pattern Examples

rule_content is consumed by each tool’s match_rule() method and auto-generated by ToolBase.generate_suggestions(). Because both methods are part of the tool interface, each tool can define its own pattern syntax and matching logic independently. For AgentScope’s built-in tools, the patterns are as follows:
Matches against the command parameter. Pattern format is COMMAND_PREFIX:* — the prefix is the leading token of the command, and * matches any arguments that follow.
PatternMatchesDoes Not Match
npm run:*npm run build, npm run testnpm install
git commit:*git commit -m "fix"git push
rm:*rm file.txt, rm -rf /tmp/xls
PermissionRule(
    tool_name="Bash",
    rule_content="npm run:*",
    behavior=PermissionBehavior.ALLOW,
    source="userSettings",
)

Configuring Rules

At initialization — pass rules into PermissionContext when creating the agent:
from agentscope.agent import Agent
from agentscope.state import AgentState
from agentscope.permission import (
    PermissionContext, PermissionMode, PermissionRule, PermissionBehavior
)

agent = Agent(
    name="my_agent",
    system_prompt="...",
    model=model,
    state=AgentState(
        permission_context=PermissionContext(
            mode=PermissionMode.DEFAULT,
            allow_rules={
                "Bash": [PermissionRule(tool_name="Bash", rule_content="npm run:*",
                                        behavior=PermissionBehavior.ALLOW, source="userSettings")],
                "Write": [PermissionRule(tool_name="Write", rule_content="src/**",
                                         behavior=PermissionBehavior.ALLOW, source="userSettings")],
            },
            deny_rules={
                "Bash": [PermissionRule(tool_name="Bash", rule_content="rm:*",
                                        behavior=PermissionBehavior.DENY, source="userSettings")],
            },
        )
    ),
)
At runtime via suggestions — when the permission system returns ASK, it auto-generates suggested rules from the current call. Pass accepted rules back in UserConfirmResultEvent.rules; the agent adds them to the engine automatically:
from agentscope.event import UserConfirmResultEvent

# The ASK decision includes suggested_rules generated from the current call.
# To accept a suggestion, include it in the result event:
result = UserConfirmResultEvent(
    confirmed=True,
    rules=[suggested_rule],  # accepted rules are persisted to the engine
)

Built-in Checks

Each tool implements a check_permissions() method that runs against the actual call inputs at runtime. AgentScope’s built-in tools cover three areas:
  • Dangerous path protectionWrite, Edit, and Bash check whether the target file or command touches sensitive paths. Returns a bypass-immune safety ASK that is honored in DEFAULT/ACCEPT_EDITS/DONT_ASK (allow rules cannot silence it). BYPASS mode skips it on purpose.
  • Read-only command detectionBash parses the command string to detect read-only operations and auto-allows them in every mode (including DEFAULT). For input-dependent tools like Bash, this is exposed via the check_read_only() method (see below).
  • ACCEPT_EDITS modeWrite and Edit auto-allow operations on files within configured working directories. Bash additionally requires that every target path of a filesystem command (mkdir/touch/rm/cp/mv/sed, …) resolves inside a working directory.

Custom tools

A custom tool implements check_permissions() to add tool-specific permission logic. Tools whose read-only status depends on the input (like Bashls is read-only, rm is not) should also override check_read_only().
from agentscope.tool import ToolBase
from agentscope.permission import PermissionContext, PermissionDecision, PermissionBehavior

class MyTool(ToolBase):
    name = "MyTool"
    # Static default. For tools whose answer depends on input, leave
    # this as the conservative default and override check_read_only().
    is_read_only = False

    async def check_read_only(self, tool_input: dict) -> bool:
        """Optional: dynamic read-only check.

        Defaults to returning self.is_read_only. Override when whether
        an invocation modifies state depends on the input. The engine
        calls this to decide auto-allow in EXPLORE and ACCEPT_EDITS.
        """
        return tool_input.get("operation") in {"list", "describe", "get"}

    async def check_permissions(
        self,
        tool_input: dict,
        context: PermissionContext,
    ) -> PermissionDecision:
        target = tool_input.get("target")

        # Custom safety check: block operations on production resources.
        # Setting bypass_immune=True makes this ASK survive allow rules
        # in DEFAULT/ACCEPT_EDITS/DONT_ASK; BYPASS still skips it.
        if target and target.startswith("prod-"):
            return PermissionDecision(
                behavior=PermissionBehavior.ASK,
                message=f"Operation targets production resource: {target}",
                decision_reason="Safety check: production resource",
                bypass_immune=True,
            )

        # Return PASSTHROUGH to let the engine continue with rules/mode
        return PermissionDecision(behavior=PermissionBehavior.PASSTHROUGH)

Safety check contract

A safety check is a tool-emitted ASK that the tool considers too dangerous to be silently overridden — e.g. Write to ~/.bashrc, Bash with rm -rf /. Setting bypass_immune=True on the decision asks the engine to surface the ASK to the user even when an allow rule matches or the mode would otherwise auto-allow. Use it whenever a wrong call would cause damage the user almost certainly didn’t intend. Example: a custom DeployTool returns bypass_immune=True when the target is prod-*, so a blanket allow_rules["DeployTool"] = ["*"] configured for staging cannot accidentally authorize a production deploy. The exact handling per mode:
Modebypass_immune=True ASK is…
DEFAULThonored — allow rules cannot override it
ACCEPT_EDITShonored — same as DEFAULT
EXPLOREnot applicable (the engine does not call check_permissions in EXPLORE; the read-only verdict is final)
BYPASSignored — BYPASS skips all safety ASKs by design
DONT_ASKconverted to DENY (no user available to answer)
A regular ASK (bypass_immune=False, the default) can be overridden by a matching allow rule in DEFAULT/ACCEPT_EDITS, and is silently allowed by BYPASS’s fallback.

Read-Only Commands

Common read-only bash commands are auto-allowed without any rules, in every mode (including DEFAULT). A compound command (&&, ||, ;, |) is read-only only if all subcommands are read-only. Output redirections (>, >>) always make a command non-read-only.
CategoryCommands
Gitgit status, git log, git diff, git show, git branch, git blame, git grep, git reflog, git config --list
Filesls, cat, head, tail, grep, rg, find, tree, stat, wc, pwd, which
Dockerdocker ps, docker images, docker logs, docker inspect, docker info
GitHub CLIgh repo view, gh issue list, gh pr list, gh status
Package managersnpm list, pip list, pip show, node --version, python --version

Dangerous Path Protection

Operations targeting the following paths trigger a bypass-immune ASK in DEFAULT, ACCEPT_EDITS, and DONT_ASK (converted to DENY in DONT_ASK). BYPASS mode explicitly skips this check — if you need dangerous-path protection while running in BYPASS, add deny rules for the specific paths.
CategoryPaths
Shell configs.bashrc, .zshrc, .bash_profile, .profile
Git configs.gitconfig, .gitmodules
SSH.ssh/config, .ssh/authorized_keys, id_rsa, id_ed25519
Credentials.env, .env.local, .npmrc, .pypirc, .aws/credentials
Directories.git/, .ssh/, .claude/, .vscode/, .aws/, .kube/

Common Recipes

The following examples show how to configure AgentState.permission_context for common deployment scenarios. Each recipe combines a mode with rules to match a specific use case.
# EXPLORE mode: agent can freely use read-only tools (Read, Grep,
# Glob) and read-only bash commands (`ls`, `git status`, `cat`, ...).
# Any modification — Write, Edit, or non-read-only bash command — is
# denied automatically.
agent = Agent(
    name="explorer",
    system_prompt="...",
    model=model,
    state=AgentState(
        permission_context=PermissionContext(mode=PermissionMode.EXPLORE)
    ),
)