Skip to content

Permission Rules

Precisely constrain what CodeBuddy Code can do with fine-grained allow / ask / deny rules, Permission Modes, and layered settings. Rules can be committed to the repository and shared with the team, or overridden locally by developers.

Permission System Overview

CodeBuddy Code uses a layered evaluation mechanism to balance capability and security. Before each tool call, the following eight phases are evaluated in order:

PhaseCheckBehavior on match
1Deny rulesReject immediately, with the highest priority
2Trusted Allow rules (user / cli / flag / session / policy / project under a trusted directory / --allowedTools)Allow immediately, and can bypass command safety checks
3Command safety check (interactive only)Force confirmation for high-risk / dangerous commands
4Ask rulesForce confirmation
5Bypass mode short-circuitAllow directly in bypassPermissions mode; downgrade to default when disableBypassPermissionsMode is enabled
6Untrusted Allow rules (project / local outside trusted directories, plus command / sandbox sources)Allow, but cannot bypass command safety checks
7Permission Mode strategyDecide whether approval is required based on the current Permission Mode
8dontAsk / async agent fallbackConvert ask to deny in print / async agent scenarios to avoid deadlock

Key point: deny always takes precedence, and allow is split into two layers: "trusted" and "untrusted". Before the project root is listed as a trusted directory, allow rules in .codebuddy/settings.json cannot bypass the command safety gate. This is designed to prevent malicious repositories from bypassing local safety protections by pushing their own settings into team PRs.

Read-only tools (Read / Grep / Glob, etc.) do not prompt for approval by default inside trusted directories. Editing tools and Bash always go through the full phase chain.

Three Rule Behaviors

The three arrays under the permissions object correspond to three behaviors:

json
{
  "permissions": {
    "allow": ["Bash(npm test)", "Read(/tmp/data/**)"],
    "ask":   ["WebFetch"],
    "deny":  ["Bash(rm -rf *)", "Edit(.git/**)"]
  }
}
  • allow: CodeBuddy can use it without asking for approval
  • ask: Ask for approval every time it is used
  • deny: Never allow it to be used

Where to Manage Rules

/permissions command

Enter /permissions in a session to open the permission management panel. You can view all current allow / ask / deny rules, which settings layer they come from, and temporarily add or remove them (written to the user, project, or project-local scope).

When "Yes, don't ask again" is selected in the dialog, CodeBuddy writes the most stable prefix for the current command into the allow array of the corresponding scope's settings.

CLI Startup Arguments

ArgumentPurpose
--allowedTools <tools...>Process-level temporary allow rules. Separated by spaces or commas. Example: --allowedTools "Bash(git:*) Edit"
--disallowedTools <tools...>Process-level temporary deny rules. Same as above
--add-dir <path>Add an extra directory to the trusted directory scope (affects whether Read requires confirmation)
-y / --dangerously-skip-permissionsEquivalent to --permission-mode bypassPermissions

Configuration Files

See Settings Configuration for details. CodeBuddy merges these four scopes:

ScopePath
user~/.codebuddy/settings.json
project<repo>/.codebuddy/settings.json (committed to git)
project-local<repo>/.codebuddy/settings.local.json (not committed to git, local override)
cliArg / flagSettings / session / policySettingsProcess state, not persisted to disk

Rule Syntax

Rule form: Tool or Tool(specifier).

Match an Entire Tool

Remove the parentheses to match all calls to a tool:

RuleMeaning
BashAll Bash commands
WebFetchAll web fetches
ReadAll file reads
EditAll file edits

* can also be used as a standalone rule, equivalent to a full-match wildcard.

Add a Specifier for Fine-Grained Matching

Write arguments inside the parentheses:

RuleMatches
Bash(npm run build)Exact match for npm run build
Bash(npm:*) or Bash(npm *)All commands starting with npm
Read(./.env).env in the current directory
Edit(/src/**/*.ts)src/**/*.ts under the project root
Read(~/.zshrc).zshrc in the user's home directory
Read(//tmp/scratch.txt)Absolute filesystem path /tmp/scratch.txt
WebFetch(domain:example.com)Fetch example.com
mcp__puppeteer__navigateThe navigate tool of the MCP puppeteer service
Agent(Explore)The Explore subagent

Tool-Specific Rules

Bash

Bash rules support three syntax forms:

SyntaxMeaningExample
Exact matchThe pattern is exactly equal to the commandBash(npm run build) only matches npm run build
:* prefixThe pattern ends with :* → matches the first word / multi-word prefix of the commandBash(git:*) matches git status / git push origin main
WildcardWhen the pattern contains *, matches as a bash glob pattern (* can cross /)Bash(npm run *) matches npm run build; Bash(ls *) matches ls -al /tmp/x

The wildcard mode deliberately lets * cross /. Otherwise, ls * could not match ls -al /xxx, which is one of the most common pitfalls for users.

Compound Commands

CodeBuddy parses shell operators && / || / ; / | and evaluates each subcommand independently:

  • deny / ask rules: trigger if any subcommand matches
  • allow rules: require all subcommands to match before allowing — a compound command with one matching and one non-matching subcommand will still ask for approval, preventing attackers from hiding dangerous commands next to allowed ones

Example:

text
allow: ["Bash(git:*)"]

git status         → allow
git status && rm * → ask (rm * is not in allow; every subcommand must match)
git status; rm *   → ask (same as above)

Redirection

Commands containing > / < / >> / << / &> require an exact match under allow rules; wildcard rules do not apply.

Read / Edit / Write

File rules use glob matching and perform three layers of path normalization:

patternExplanationExample
//pathAbsolute filesystem pathRead(//etc/hosts)
~/pathStarting from the user's home directoryRead(~/.zshrc)
/pathStarting from the project rootEdit(/src/**/*.ts)
path or ./pathStarting from the current working directoryRead(.env)

Matching options: dotfiles allowed, case-insensitive, bare filenames can match at any depth.

Notes:

  • deny rules such as Edit(.git/**) block all attempts through Edit / Write / NotebookEdit, but do not block indirect paths through Bash such as python -c 'open(".git/config", "w")...'. OS-level protection depends on Bash sandboxing
  • Read rules are also intercepted and parsed for some Bash file-read commands (cat, head, tail, etc.)

WebFetch

text
WebFetch                       # any URL
WebFetch(domain:example.com)   # only example.com and its subdomains

Supports the domain: prefix for hostname matching (including subdomains).

MCP Tools

MCP tools are named in the format mcp__<server>__<tool>. Rules support three granularities:

RuleMatches
mcp__puppeteerAll tools of the entire puppeteer server
mcp__puppeteer__*Same as above (wildcard form)
mcp__puppeteer__navigateOnly the navigate tool

Tool names starting with mcp__ are matched as MCP rules.

Agent (Subagent)

json
{
  "permissions": {
    "deny": ["Agent(Explore)", "Agent(Plan)"]
  }
}

You can also use a CLI flag:

bash
codebuddy --disallowedTools "Agent(Explore) Agent(Plan)"

After being denied, that subagent_type will be rejected when the main agent invokes the Agent tool.

Skill

json
{
  "permissions": {
    "deny": ["Skill(dangerous-skill-name)"]
  }
}

Skill rules must be exact matches; wildcards are not supported.

Trusted Directories

By default, CodeBuddy only considers the current working directory trusted. The Read tool is allowed inside trusted directories and asks outside them; Edit / Bash always go through full approval (unless an allow rule applies or a permissive mode is active).

Several ways to expand the trusted scope:

MethodPersistence
--add-dir <path> startup argumentProcess-level
/add-dir command within a sessionSession-level
permissions.additionalDirectories settingPersistent
permissions.trustedDirectories settingPersistent

The effective trusted directory list = workspace root + settings.trustedDirectories + directories added at startup with --add-dir or in-session with /add-dir.

Both --add-dir and permissions.additionalDirectories only grant file access. They do not make CodeBuddy load .codebuddy/ configuration from those directories (agents / hooks / settings, etc. still use the startup directory).

Project Directory Trust Switch

Whether you have "explicitly trusted" a repository directory also affects the trust level of allow rules. When the directory is not trusted:

  • allow rules in <repo>/.codebuddy/settings.json and .codebuddy/settings.local.json are classified into the untrusted layer (Phase 6), and cannot bypass command safety checks
  • After the user confirms trust in the interactive UI, project-level rules are promoted to the Phase 2 trusted layer

This is designed to reduce lateral risk that can arise immediately after cloning and running a repository.

Protected Files / Paths

In any mode, CodeBuddy adds extra protection for a set of critical paths (matching Permission Modes):

  • The repository itself: .git, .gitconfig, .gitmodules
  • Shell configuration: .bashrc / .zshrc / .envrc, etc.
  • Package management: .npmrc / .yarnrc / bunfig.toml, etc.
  • IDE tools: .vscode / .idea / .husky / .devcontainer
  • CodeBuddy itself: .codebuddy (except .codebuddy/worktrees)
  • MCP / configuration: .mcp.json / .codebuddy.json

bypassPermissions mode still allows most of these through, but "catastrophic commands" such as rm -rf / / rm -rf ~ are forced to ask.

Extending Permissions with Hooks

The PreToolUse hook in the Hooks system runs before permission approval prompts and can programmatically allow / deny / rewrite inputs.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "/path/to/bash-policy.sh" }]
      }
    ]
  }
}

Hook exit code semantics:

Exit codeBehavior
0 + JSON decisionExecute according to permissionDecision (allow / ask / deny) in the JSON
2Block (stderr content is passed back to the model)
Other non-0Non-blocking error (shown but allowed)

Notes:

  • deny / ask rules always take effect before hooks. A hook returning "allow" cannot bypass deny in settings
  • However, blocking hooks (exit code 2) can short-circuit allow rules before Phase 1, so you can "allow all Bash first, but use hooks to separately block a few specific commands"

Working with Sandboxing

Permission rules and Bash sandboxing are complementary layers:

  • Rule layer: constrains whether CodeBuddy "wants to use" a tool or access a path
  • Sandbox layer: constrains whether the Bash subprocess can "actually access" a resource at the OS layer

Typical defense-in-depth combinations:

  • deny rules prevent CodeBuddy from actively attempting restricted tools
  • The sandbox prevents all Bash subprocesses from reaching files / networks outside the allowlist — even if prompt injection makes CodeBuddy want to bypass it, it cannot
  • WebFetch domain: allow rules and sandbox allowedDomains both apply, and the final boundary is the intersection

Settings Priority

Permission rules inherit the general Settings priority:

flagSettings / cliArg / session  > userSettings > policySettings >
projectSettings > localSettings > command/sandbox sources

However, the evaluation order is more important than settings priority:

  • deny arrays are merged from all scopes, and a deny in any scope rejects
  • allow arrays are merged twice by "trusted / untrusted": user / cli / flag / session / policy are always included in the trusted merge; project / local are considered trusted only when the directory is trusted
  • disableBypassPermissionsMode takes effect when it is "disable" in any of the four layers: user / project / local / CLI startup flag

Example Configurations

Minimized Trust: Only Allow npm Tests + Reading Project Files

json
{
  "permissions": {
    "defaultMode": "default",
    "allow": [
      "Bash(npm test)",
      "Bash(npm run lint)",
      "Read(/src/**)",
      "Read(/test/**)"
    ],
    "deny": [
      "Bash(rm:*)",
      "Bash(curl:*)",
      "Bash(wget:*)",
      "Edit(.git/**)",
      "Edit(/.codebuddy/**)"
    ]
  }
}

CI / Pipeline Scenario: Skip Approval + Strong Deny

json
{
  "permissions": {
    "defaultMode": "bypassPermissions",
    "deny": [
      "Bash(rm -rf /:*)",
      "Bash(sudo:*)",
      "Bash(curl * -o /etc/*)",
      "WebFetch(domain:internal-corp.example)"
    ]
  }
}

Team Shared + Personal Relaxation

<repo>/.codebuddy/settings.json (committed to git):

json
{
  "permissions": {
    "deny": ["Bash(rm:*)", "Edit(.git/**)"]
  }
}

~/.codebuddy/settings.json (user-level, private):

json
{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": ["Bash(git:*)", "Bash(npm:*)"]
  }
}