Permissions
Permission modes, permission rules, protected paths, auto mode and the audit trail.
Before any tool runs, Cusp decides whether it may run without asking you. The decision comes from, in order: hooks, your rules, protected paths, the session's permission mode, and in auto mode a classifier model. If none of them allows the call, Cusp asks you.
Permission modes
| Mode | What runs without asking | Switch to it |
|---|---|---|
default | Read-only actions inside the workspace: reading and searching files, read-only shell commands such as ls or git status | The default |
acceptEdits | The above, plus file edits inside the workspace | Shift+Tab, /mode acceptEdits |
plan | Read-only exploration only. Edits and mutating commands are denied until you approve a plan | Shift+Tab, /plan |
auto | Low-risk actions, as judged by a fast classifier model. Risky ones still ask | Shift+Tab, /mode auto |
bypass | Everything except deny rules and protected paths | --permission-mode bypassPermissions at start |
In the CLI, Shift+Tab cycles through default, acceptEdits, plan and auto. Bypass is in the cycle only if the session started in it.
There is also dontAsk, for unattended runs: anything that would have prompted is denied instead. Headless cusp -p runs behave this way unless you pre-approve tools.
Plan mode
In plan mode the agent explores, then calls ExitPlanMode with a plan. You approve it (and pick the mode to continue in) or send it back with feedback. While planning, the only file the agent may write is its own plan file. The agent can also enter plan mode itself with EnterPlanMode when a task is large enough to need a design first.
Auto mode
Auto mode hands the "should I ask?" question to a separate, fast model (the provider's small model). The classifier sees only your own recent messages and the proposed action. It never sees the agent's reasoning or tool output, so text injected into a web page or file can't argue it into approving something.
The classifier fails closed. If it's unavailable, errors, or answers ambiguously, Cusp asks you. After 3 consecutive blocks, or 20 in one session, auto mode pauses and falls back to prompting, so a confused agent can't grind against it. Broad allow rules such as Bash(*) are ignored in auto mode, because they would bypass the classifier entirely.
Auto mode is available only if your user settings allow it. Project settings can't turn it on.
Rules
Rules let you pre-approve, always ask, or always deny specific actions. The syntax is Tool or Tool(specifier), the format existing settings.json rules already use.
{
"permissions": {
"allow": [
"Bash(npm run test:*)",
"Bash(git log *)",
"Edit(src/**)",
"WebFetch(domain:docs.python.org)",
"mcp__github__get_issue"
],
"ask": ["Bash(git push *)"],
"deny": ["Read(.env)", "Read(**/*.pem)", "Bash(curl *)", "WebSearch"]
}
}Evaluation order
- Deny rules win over everything. A deny matches in every mode, including bypass.
- Ask rules come next. They force a prompt even if an allow rule also matches.
- Allow rules approve the call.
- If no rule matches, the mode decides.
Rules from every settings file are combined: managed, command line, local, project and user. See Configuration for where they live.
Rule reference
| Rule | Applies to | Specifier |
|---|---|---|
Bash(…) | Bash, and shell commands run through Terminal and Process | A command pattern |
Read(…) | Read, Grep, Glob, LS | A path pattern |
Edit(…) | Edit, Write, MultiEdit, ApplyPatch, EditLines, NotebookEdit | A path pattern |
WebFetch(domain:…) | WebFetch | A host name |
WebSearch(…) | WebSearch | A query glob |
Agent(…) (alias Task) | Agent | A subagent type, such as Agent(Explore) |
Skill(…) | Skill | A skill name: Skill(deploy), Skill(deploy *), Skill(ops:*) |
mcp__server, mcp__server__tool, mcp__server__* | MCP tools | — |
| Any other tool name | That tool | — |
An Edit(…) allow rule also grants read access to the same paths. A Read(…) deny rule also blocks edits there.
Command patterns
| Pattern | Matches |
|---|---|
Bash(npm test) | Exactly npm test |
Bash(npm run test:*) | npm run test followed by anything (the trailing :* is a prefix marker) |
Bash(git log *) | git log and git log --oneline, but not git logx |
Bash(ls*) | ls, lsof, anything starting with ls |
Cusp parses shell commands properly before matching. A compound command such as npm test && git push is split, and each part has to be allowed on its own. Command substitutions, heredocs, pipes and wrappers such as env, timeout or xargs are understood, so timeout 60 rm -rf build is treated as rm -rf build.
If you write Bash(git * main), Cusp warns that the wildcard before the subcommand matches far more than you probably intended.
Path patterns
Path patterns follow gitignore conventions:
| Pattern | Meaning |
|---|---|
src/** or ./src/** | Relative to the working directory |
/docs/** | Relative to the root of the settings file's project (for user settings, ~/.cusp) |
~/notes/** | Relative to your home directory |
//etc/hosts | An absolute path |
.env, *.pem | A bare name matches at any depth |
!pattern | Carves an exception out of an earlier rule from the same file |
* matches within a path segment, ** across segments, and a pattern that names a directory covers everything below it.
Protected paths
Some files are how a prompt injection would persist or escalate: agent configuration, git internals, shell startup files and tool configs that execute code. Writes to them always need a person, in every mode, bypass included. In dontAsk and headless runs they are denied.
Protected directories, at any depth: .git, .cusp, .claude, .codex, .cursor, .gemini, .vscode, .idea, .husky, .cargo, .devcontainer, .yarn, .mvn, .ssh, .gnupg, .aws.
Protected files, at any depth, include .gitconfig, .gitmodules, shell startup files (.bashrc, .zshrc, .profile and friends), .envrc, .npmrc, .yarnrc, bunfig.toml, .pre-commit-config.yaml, lefthook configs, .mcp.json, .claude.json and .cusp.json.
A few locations inside protected directories are exempt because agents legitimately write there, such as .cusp/worktrees and .cusp/agent-memory.
Cusp also refuses to run obviously destructive shell commands against critical paths, such as removing your home directory or the filesystem root, without an explicit confirmation.
Answering a prompt
When Cusp asks, you can choose:
| Choice | Effect |
|---|---|
| Allow once | This call only |
| Allow for this session | Adds a session rule, so similar calls run without asking until the session ends |
| Always allow | Writes a rule to .cusp/settings.local.json, which is not meant to be committed |
| Deny | The call fails and the agent is told it was denied |
| Deny with feedback | The call fails and the agent receives your explanation, so it can change course |
Cusp suggests a rule for "always" that is as narrow as is useful, for example Bash(npm run lint:*) rather than Bash(npm *).
Hooks and permissions
PreToolUse hooks run before the permission engine and can return allow, deny, ask or defer. A hook's deny holds in every mode. A PermissionRequest hook runs when a prompt would be shown and can answer it for you. See Hooks.
Workspace trust
The first time you open a folder, Cusp asks whether you trust it. Until you do, the project's own settings files can only narrow permissions. They can't add allow rules, hooks, environment variables, MCP servers, additional directories, an API key helper or a status line command.
Independently of trust, project and local settings can never set the default mode to auto or bypass, and can't override sensitive environment variables such as HOME, TMPDIR or XDG_*.
Audit
Every decision is recorded as a permission.decision event in the session log, with the tool, the mode, the outcome, what decided it (a rule, the mode, the classifier, a protected path, a hook, or you) and the matching rule. You can review any session's decisions after the fact.
Command-line flags
cusp --permission-mode acceptEdits
cusp --allowedTools "Bash(git log *)" Edit
cusp --disallowedTools "Bash(rm *)" WebFetch
cusp --dangerously-skip-permissions # same as --permission-mode bypassPermissions--allowedTools and --disallowedTools accept comma- or space-separated lists and never split inside parentheses. They apply to the session only.
Sessions
A session is an append-only log of events, which is what makes resume, fork, rewind and worktree isolation possible.
Sandbox
Shell commands the agent runs are contained by the operating system sandbox — Seatbelt on macOS, bubblewrap on Linux — with workspace-only writes, no credential reads and no network unless you allow it.