Permission rules

Before a tool call runs, Syntic Code decides one of three things: run it, ask you, or refuse it. Two things shape that decision: the mode you are in, and the rules you have written.

Modes

The mode sets what happens when no rule matches: default, auto-edit, plan, auto, bypass or don’t-ask. How each one behaves, how to switch, and which paths always ask are covered on Permission modes.

The approval prompt

When a call needs your OK, you see what it wants to do (the command, or a diff for an edit) and three choices:

  1. Yes: run it this once.
  2. Yes, and don't ask again for …: the grant fits the call. For a shell command it covers its leading words (for example npm run test); for edits, that folder for this session; for a web fetch, that host. Shell and fetch grants land in .syntic/settings.local.json. Commands that start other programs (shells, interpreters, sudo, xargs), deletions, and lines with redirects, substitutions or quotes are never offered this choice.
  3. No, and say what to do instead: refuse and type guidance for the agent.

In a non-interactive run (-p), anything that would ask is refused instead, and the agent is told why.

The final arguments are checked again

What runs is always what was checked. Two things can change a call after its first check:

  • a PreToolUse hook that rewrites the tool’s input;
  • you, editing the call in the approval prompt before saying yes.

Either way, the changed call goes through the whole decision again before it runs: deny, ask and allow rules, the mode, the tool’s own safety checks and, in auto mode, the auto mode checklist. If the new arguments are refused, the call does not run, the agent is told why, and PermissionDenied hooks fire. If they now need your OK, you are asked about the changed call.

The same goes for anything that changes while a prompt is open. If you switch mode (say into plan mode with Shift+Tab), edit rules in /permissions, or a settings file is reloaded before you answer, your “yes” is checked against the new situation: a call the new rules deny is refused, and a call a new ask rule matches asks again (a remembered “don’t ask again” cannot answer it).

Rules

Rules are short patterns in three lists:

ListA matching call
allowRuns without asking.
askAlways asks, in auto-edit and auto mode too, and wins over a matching allow rule. A saved “don’t ask again” never answers it.
denyIs refused, in every mode, including bypass.

Put them in any settings file under permissions:

{
  "permissions": {
    "allow": ["Bash(cargo test:*)", "Bash(cargo clippy:*)", "WebFetch(domain:docs.rs)"],
    "ask":   ["Bash(git push:*)"],
    "deny":  ["Read(**/.env)", "WebFetch(domain:pastebin.com)"]
  }
}

Rules from every settings file add up: user, project, local and organization policy. Untrusted folders: project and local files are skipped. For one run, use --allowed-tools "Bash(make:*),Read" and --disallowed-tools "WebFetch"; disallowed wins over allowed.

Rule patterns

PatternMatches
BashEvery shell command. Any tool name works the same way (Read, Edit, WebFetch, …).
*Every tool.
Bash(npm test)Exactly npm test.
Bash(git commit:*)git commit alone or with any arguments.
Bash(npm run *)A wildcard: * matches any text. A trailing * also matches the bare npm run.
Read(.env), Edit(src/**)File path patterns (see path rules).
WebFetch(domain:example.com)Fetches from that host and its subdomains.
mcp__github or mcp__github__*Every tool from one MCP server.
mcp__github__create_issueOne MCP tool by its full name.
mcp__db__query(database:prod*)A rule on one input field of a tool.
Agent(explore)Starting a particular subagent.
Skill(deploy)Running a particular skill.

Tool names ignore case. Escape a literal (, ) or \ with a backslash. A rule you type into /permissions is checked and refused with an explanation if it is malformed.

For shell commands, leading VAR=value assignments are ignored, and a rule for curl also matches /usr/bin/curl. A deny rule matches when any part of a chained command (a && b, a | b) matches, including after sudo.

Rules on one input field

Most rules test a tool’s main argument: a command, a path, a URL. A rule written as Tool(field:value) tests a named input field instead, when the call has a field of that name. * in the value matches any run of characters. Numbers and booleans are compared as written in JSON (true, 3).

RuleMatches
Agent(model:Syntic Code Fast)Subagents started on that model.
mcp__db__query(database:prod*)The query tool of the db server, when database starts with prod.
Agent(Explore)Starting the Explore agent. A call that names no agent counts as the general-purpose one.

Because this form only applies when the field exists, familiar shapes keep their meaning: Bash(npm:*) is still a command prefix and WebFetch(domain:example.com) is still a host rule.

Path rules

Read(…) and Edit(…) rules take gitignore-style patterns. Where the pattern starts decides what it is relative to:

Written asRelative toExample
name or ./nameThe current working folderRead(./config/secrets.json)
/nameThe project (your main working folder)Edit(/src/**)
~/nameYour home folderRead(~/.ssh/**)
//nameThe file-system root (an absolute path)Read(//etc/hosts)
  • A pattern with no / in it, such as .env or *.pem, matches at any depth.
  • ** matches any number of folders: Edit(src/**/*.test.ts).
  • An Edit(…) rule also covers Write, MultiEdit and NotebookEdit.
  • Symbolic links are followed: a deny or ask rule trips if the path or anything it links to matches, and an allow rule only counts if it also covers where the link really leads.
{
  "permissions": {
    "allow": ["Edit(/src/**)", "Edit(/tests/**)"],
    "deny":  ["Read(.env)", "Read(*.pem)", "Edit(/migrations/**)", "Read(~/.ssh/**)"]
  }
}

Which rule wins

  1. Deny rules always win.
  2. Plan mode refuses any change.
  3. Ask rules ask (in don’t-ask mode, they refuse).
  4. Allow rules and your saved “don’t ask again” answers let the call run.
  5. Otherwise the mode decides.

Checks that always ask

A few checks ask in every mode, even bypass, and even when a rule allows the call:

  • a shell command that the command-safety check rates as critical;
  • a command too long to show you properly in a prompt;
  • with blockReadsOutsideWorkingDirectories on, a command that may read outside your working folders.

Commands that change something outside your machine (a git push, a cluster, cloud resources, a production deploy, a remote database) have their own rules: auto mode never runs them unattended, and every prompt for one names its target. See Commands that reach outside.

Catastrophic commands (for example a recursive delete of a system folder) are refused outright.

Manage rules with /permissions

/permissions opens a manager with six tabs:

TabShows
Allow, Ask, DenyThe rules from every settings file, each labelled with its file. Press a to add one (you choose the file), d to delete.
Auto modeAuto mode’s own rule lists (below).
Recent denialsCalls refused in this session. Press a to turn one into an allow rule.
WorkspaceThe folders this session may read and edit.

Organization policy rules are shown but cannot be edited here.

Auto mode

In auto mode, your deny, ask and allow rules apply first, then auto mode’s own rule lists below.

Auto mode’s rule lists

{
  "autoMode": {
    "allow":     ["Bash(cargo test*)", "running the project's own scripts"],
    "soft_deny": ["Bash(git push*)", "editing CI configuration"],
    "hard_deny": ["Bash(rm -rf /*)", "uploading files anywhere"]
  }
}
ListEffect
allowSkips the model review. Never overrides a deny rule.
soft_denyAsks first (refused in a non-interactive run).
hard_denyRefused outright.

Entries are either patterns like Bash(cargo test*), checked instantly, or plain sentences, which are given to the model review. A project’s own settings may add soft_deny and hard_deny entries but not allow entries. Add rules with /permissions auto <allow|soft_deny|hard_deny> <rule> (add --project for a project deny rule), and clear your own with syntic auto-mode reset.

The model review and the checklist

After the rule lists, auto mode sends the call to a quick model review, and a fixed checklist of high-impact actions always asks. Both are described on Permission modes; the list of what auto mode always stops for is there too. Organization-wide auto mode rules are covered on Configure auto mode.

Extend rules with hooks

Rules are static patterns. When a decision needs logic, such as checking a ticket number or a branch name, use hooks:

Hook eventUse it to
PreToolUseAllow, ask about, refuse, or rewrite a call before the permission check. A rewritten call is checked again in full.
PermissionRequestAnswer a prompt on your behalf, for example in a script.
PermissionDeniedReact when a call is refused: log it, or tell someone.

A deny rule still wins over a hook’s “allow”.

Example rule sets

A careful setup for a web project, kept in the project’s .syntic/settings.json so the whole team shares it:

{
  "permissions": {
    "allow": [
      "Bash(npm run test:*)",
      "Bash(npm run lint:*)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "WebFetch(domain:developer.mozilla.org)"
    ],
    "ask": ["Bash(git push:*)", "Bash(npm install:*)"],
    "deny": ["Read(.env)", "Read(.env.*)", "Read(*.pem)", "Edit(/migrations/**)"]
  }
}

A personal rule set in ~/.syntic/settings.json that applies to every project:

{
  "permissions": {
    "deny": ["Read(~/.ssh/**)", "Read(~/.kube/**)", "WebFetch(domain:pastebin.com)"],
    "ask": ["mcp__github__create_pull_request"]
  }
}

A script that may only read and run the tests, nothing else:

syntic -p "find why the date tests fail" \
  --permission-mode dontAsk \
  --allowed-tools "Read,Grep,Glob,Bash(cargo test:*)"

More complete files are on Example settings.

Safety nets

Folder trust

Each folder is untrusted until you say so. On an interactive start in a new folder you are asked whether you trust its files; anything but yes exits. Your answers are kept in ~/.syntic/trusted_folders, except for your home folder, which asks every time. The full story is on Folder trust.

Until a folder is trusted, these stay off: project settings (and their rules and env), project hooks, project MCP servers, project agents and skills, and git integration. Non-interactive runs (-p) in an untrusted folder run in this restricted state and say so. /cd asks again for each untrusted folder it moves into.

File safety

  • Read before edit. Edit refuses a file the agent has not read this session; Write refuses to overwrite an unread existing file. New files are fine.
  • No stale overwrites. If a file changed on disk after the agent read it, Write refuses until it reads the file again.
  • Stay in the workspace. File tools only write inside your working folders, and a path that escapes through a symlink is refused.
  • Sensitive files still ask in auto-edit mode: shell startup files, git config and .git/, .syntic/, .mcp.json, other tools’ config folders and credential files.
  • Keep reads inside too, if you want: "permissions": { "blockReadsOutsideWorkingDirectories": true }.

Extra folders

The session works in the folder you started in. To give it more room:

HowLasts
syntic --add-dir ../shared-libThis run.
/add-dir <path>This session. The folder’s skills, commands and agents load too.
/add-dir <path> --rememberAlso saved to this project’s local settings.
"permissions": { "additionalDirectories": ["../shared-lib"] }Every session that reads that file.

A repository’s own settings cannot add your home folder, the file-system root or credential folders.

Locked-down sessions

FlagFor
--restricted (or SYNTIC_RESTRICTED=1)No shell, code or web-fetch tools, file tools kept in this folder, no bypass, organization settings only.
--safe-modeTroubleshooting: instruction files, plugins, skills, hooks and MCP servers all off.
--bareMinimal scripted runs: no settings hooks, plugins, skill discovery or auto-memory.