Skip to content

@moxxy/plugin-security adds a capability-based sandbox layer on top of the permission engine. Tools declare what they need (filesystem globs, network hosts, env keys, wall-clock budget, memory ceiling); an Isolator enforces those bounds at every call. The plugin is a no-op until you flip security.enabled: true in config.

The SDK ships the declaration types (CapabilitySpec, ToolIsolationSpec, Isolator, ISOLATION_RANK) so plugin authors can declare isolation without taking a runtime dep on this plugin. Enforcement only kicks in when @moxxy/plugin-security is loaded and enabled.

Install ​

sh
pnpm add @moxxy/plugin-security

The @moxxy/cli binary registers it for you. Embedders use buildSecurityPlugin:

ts
import { buildSecurityPlugin } from '@moxxy/plugin-security';

const { plugin, registry, audit } = buildSecurityPlugin({
  config: {
    enabled: true,
    isolator: 'inproc',                  // default for any declared tool
    perTool: { 'Bash': 'subprocess' },   // override per tool
    perPlugin: { '@moxxy/plugin-mcp': 'worker' },
    requireDeclaration: false,           // deny tools that didn't declare?
  },
  toolRegistry: session.tools,
  isolators: [],                         // extras on top of `none` + `inproc`
});
session.pluginHost.registerStatic(plugin);

audit() returns a row per tool with its declared spec and the isolator that would actually run it — useful for moxxy security audit.

Declaring isolation on a tool ​

ts
import { defineTool, z } from '@moxxy/sdk';

export const fetchTool = defineTool({
  name: 'web_fetch',
  description: 'Fetch a URL and return the body.',
  inputSchema: z.object({ url: z.string().url() }),
  isolation: {
    // Author's minimum acceptable strength. If the user picks a weaker
    // isolator, the security plugin denies the call rather than silently
    // running under-isolated.
    required: 'inproc',
    capabilities: {
      net: { mode: 'allowlist', hosts: ['*.example.com'] },
      fs: { read: ['$cwd/**'] },         // `$cwd` resolves at call time
      env: ['HOME'],                     // every other env var is masked
      timeMs: 30_000,                    // wall-clock budget; aborts via ctx.signal
      memMb: 256,                        // soft ceiling (honored where supported)
      subprocess: false,                 // may NOT spawn children
    },
  },
  permission: { action: 'prompt' },
  handler: async ({ url }) => { … },
});

The declaration is advisory until the user enables the security plugin. With it enabled, every call to the tool funnels through the configured isolator, which enforces the capabilities.

Capability surface ​

FieldTypeEffect
fs.read / fs.writestring[] of globsPath access allowlist. $cwd prefix resolves to ToolContext.cwd.
net.mode'none' | 'any' | 'allowlist'Network policy. Allowlist takes a list of host patterns.
envstring[]Env vars the tool may read. Informational under inproc (the handler shares the parent's process.env); enforceable by subprocess/worker isolators that can spawn with a restricted env.
timeMsnumberWall-clock budget. Aborted via ctx.signal. Enforced by every isolator.
memMbnumberSoft memory ceiling. Honored by isolators that support it (e.g. worker_threads via resourceLimits).
subprocessbooleanWhether the tool may spawn child processes. Default false. Informational under inproc.

Isolators ​

Built-in:

NameStrengthWhat it does
none0Passthrough — no enforcement. Used when security.enabled: false or when an author explicitly opts out.
inproc1In-process. Validates capabilities on entry (checkAllCaps) and wraps execution in a timeMs deadline + abort. Memory ceiling not enforced.
worker2worker_threads-based (shipped in @moxxy/isolator-worker). Re-imports the tool's handlerModule in a fresh JS thread; enforces memMb via resourceLimits, timeMs + abort via worker.terminate(). Requires handlerModule to be declared on the tool.

Stronger isolators (worker / subprocess / vm / wasm / docker) implement the same Isolator interface — no SDK changes when adding new ones. They register at plugin construction time by passing into buildSecurityPlugin({ isolators: [...] }):

ts
import { buildSecurityPlugin, type Isolator } from '@moxxy/plugin-security';

export const workerIsolator: Isolator = {
  name: 'worker',
  strength: 'worker',
  async run(call, handler, caps, signal) {
    // Marshal (input) → output across a worker_threads boundary,
    // applying caps before the call resolves.
  },
};

// At setup time:
const { plugin } = buildSecurityPlugin({
  config: { enabled: true, isolator: 'worker' },
  toolRegistry: session.tools,
  isolators: [workerIsolator],
});

See .claude/agents/isolator-author.md for the full author guide — particularly the section on handler marshalling, which is the hard problem any out-of-process isolator must solve before its strength claim is real.

Per-tool / per-plugin overrides ​

Configure stricter isolation for specific tools without raising the floor for everything:

ts
// moxxy.config.ts
export default defineConfig({
  security: {
    enabled: true,
    isolator: 'inproc',          // default for declared tools
    perTool: {
      'Bash': 'subprocess',      // shell tools deserve their own process
      'web_fetch': 'worker',
    },
    perPlugin: {
      '@moxxy/plugin-mcp': 'worker',   // every MCP-sourced tool
    },
  },
});

Resolution order: perTool → perPlugin → global isolator → default inproc.

Strictness gate ​

requireDeclaration: true denies any tool call whose ToolDef has no isolation spec. Useful for hardened production runs where you want to refuse to call unknown / unaudited tools. Default false (tools without declarations run in none mode and bypass enforcement).

CLI ​

sh
moxxy security status          # is the plugin enabled? which isolators are registered?
moxxy security audit           # per-tool report: declared spec + resolved isolator
moxxy security isolators       # list registered isolators with their strength

When to enable ​

  • Production / always-on (moxxy serve --background on a shared box) — turn it on. Webhook deliveries + scheduled fires are exactly the kind of unattended path where capability bounds pay rent.
  • Solo dev TUI — usually unnecessary. The permission engine plus per-tool prompts already cover the threat model.
  • Mixed plugins — turn it on if you install plugins you didn't author. requireDeclaration: false keeps your own un-isolated tools working while still enforcing bounds on plugins that opt in.

See also ​

  • @moxxy/sdk — CapabilitySpec, Isolator, ISOLATION_RANK.
  • moxxy serve — pair always-on serve with security.enabled: true for unattended hardening.

Open source · self-hosted · MIT