Every tool call passes through a PermissionResolver. The Channel owns that resolver — the TUI pops an Ink dialog, the Telegram bot ships an inline keyboard, the HTTP channel checks a static allow-list — but the underlying policy file is shared.
The policy file
Location: ~/.moxxy/permissions.json. Edit it via moxxy perms:
moxxy perms # interactive Ink editor (TTY only)
moxxy perms list # print current policy
moxxy perms allow Read "read-only file ops"
moxxy perms allow "Bash:git *" "git is fine"
moxxy perms deny "Bash:rm -rf *" "obvious foot-gun"
moxxy perms remove Read
moxxy perms clear --yes
moxxy perms pathdeny rules win over allow rules. The name field supports glob-style matching against the tool name (* matches anything inside one segment).
Built-in resolvers
@moxxy/core exports four pre-built PermissionResolvers:
| Resolver | Use case |
|---|---|
autoAllowResolver | Test harness; allow everything without prompting. |
denyByDefaultResolver | Refuse anything not on the file policy's allow list. |
createAllowListResolver(names) | Static allow-list (the HTTP channel uses this). |
createCallbackResolver(fn) | Custom logic — your callback returns the decision. |
The TUI's resolver wraps the file policy with an interactive prompt. Persistent deny rules are evaluated first. When no policy rule decides the call, built-in Read, Glob, and Grep operations are allowed silently only when their real paths remain inside the active workspace. Resolving through a symlink to a path outside the workspace does not qualify.
Consequence-first approval
For a write, command, or access outside the workspace, the TUI leads with a human description of the target and impact. Raw tool input is hidden until you press D for details.
| Key | Decision |
|---|---|
Enter / Y | Allow this call once. |
A | Allow the same file, location, or exact command and working directory for this run. |
Esc / N | Deny the call. |
D | Toggle raw tool and input details. |
The TUI does not turn an approval into permanent policy. Use moxxy perms when you intentionally want a persistent allow or deny rule. Other channels may offer their own policy controls; the shared engine still gives deny precedence.
Deny-by-default for headless runs
Headless runs (moxxy -p ...) have no human to click. They inherit the file policy and the same tool-declared safe-workspace rules, then deny anything else. Two explicit escape hatches remain:
moxxy -p "..." --allow-tools Read,Glob,Grep # one-off allow-list
moxxy -p "..." --allow-all # everything (use with care)For the HTTP channel, set channels.http.allowedTools in your config — the channel refuses to start without it. See HTTP channel.
Plugin-level overrides
Plugins can short-circuit a call via the onToolCall hook:
hooks: {
onToolCall: async ({ call }) => {
if (call.name === 'Bash' && /rm -rf/.test(String(call.input.command))) {
return { action: 'deny', reason: 'destructive command blocked' };
}
return { action: 'allow' };
},
}Hook denies fire before the user-facing resolver sees the call — useful for audit / guardrail plugins.
Where it's implemented
- Engine:
packages/core/src/permissions/. - TUI dialog:
packages/plugin-cli/src/components/PermissionDialog.tsx. - Telegram inline keyboard:
packages/plugin-telegram/src/channel/permission-prompt.ts. - HTTP allow-list:
packages/plugin-channel-http/src/channel.ts.