Skip to content

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:

sh
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 path

deny 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:

ResolverUse case
autoAllowResolverTest harness; allow everything without prompting.
denyByDefaultResolverRefuse 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.

KeyDecision
Enter / YAllow this call once.
AAllow the same file, location, or exact command and working directory for this run.
Esc / NDeny the call.
DToggle 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:

sh
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:

ts
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.

Open source · self-hosted · MIT