Permissions
Plugins declare the permissions they need in manifest.json. AMC shows these to the user during installation and enforces them at runtime -- if your plugin calls an API it has not been granted permission for, the call is rejected.
Permission Model
Permissions gate access to specific API surfaces. Some APIs are available to every plugin without any permission declaration. The full canonical list is exported at runtime as PLUGIN_PERMISSIONS from @agent-mc/plugin-sdk.
Always Available (No Permission Needed)
These APIs are available to all plugins, regardless of permissions:
| API | Interface | What it does |
|---|---|---|
| Settings | PluginSettings | Read the plugin's own settings (getAll(), get(key)) |
| Logging | PluginLogger | Write to AMC's log (info(), warn(), error(), debug()) |
| Events | PluginEvents | Emit and listen for plugin-scoped events (emit(), on()) |
| Sidebar | PluginSidebar | Update the sidebar badge and item list (setBadge(), setItems()) |
| Toast (show) | PluginToast.show() | Display in-app toast messages (success, error, info) |
TIP
Settings are read-only from the plugin's perspective. The user configures them through AMC's Settings > Plugins panel. Your plugin defines the available settings in manifest.json and reads their current values at runtime.
Permission Reference
storage
Grants access to: PluginStorage, PluginDb, PluginFs
Three related APIs for persisting data:
PluginStorage -- simple key-value store:
await ctx.storage.get('lastRun') // Read a value
await ctx.storage.set('lastRun', Date.now()) // Write a value
await ctx.storage.delete('lastRun') // Delete a value
await ctx.storage.list('config:') // List keys with prefixPluginDb -- structured database (SQLite collections defined in manifest):
// Insert a row
const row = await ctx.db.insert('tasks', {
title: 'Review PR',
priority: 1,
})
// Query with filters, ordering, and pagination
const results = await ctx.db.query('tasks', {
where: { priority: 1 },
orderBy: { created_at: 'desc' },
limit: 10,
offset: 0,
})
// Get, update, delete by ID
const task = await ctx.db.getById('tasks', 'abc-123')
await ctx.db.update('tasks', 'abc-123', { priority: 2 })
await ctx.db.delete('tasks', 'abc-123')
await ctx.db.deleteWhere('tasks', { priority: 0 })PluginFs -- sandboxed filesystem within the plugin's data directory:
await ctx.fs.writeFile('output/report.md', content)
const text = await ctx.fs.readFile('output/report.md')
const exists = await ctx.fs.exists('output/report.md')
const files = await ctx.fs.listDir('output')
await ctx.fs.deleteFile('output/report.md')WARNING
All filesystem paths are relative to the plugin's data directory. Plugins cannot access files outside this sandbox.
secrets
Grants access to: PluginSecrets
Store and read your plugin's own credentials -- API keys, database passwords, access tokens -- encrypted by the operating system's keychain (macOS Keychain, Windows DPAPI, libsecret).
await ctx.secrets.set('db-password', password)
const password = await ctx.secrets.get('db-password') // string | null
const keys = await ctx.secrets.list() // keys only, never values
await ctx.secrets.delete('db-password')Separate from storage, and deliberately so: secrets live in their own table, so a plugin granted storage but not secrets cannot read your values or even enumerate their keys.
WARNING
set throws on a machine with no available keyring rather than falling back to a plaintext write, and get returns null for both "never set" and "no longer decryptable". See the Secrets API before you rely on either.
sessions
Grants access to: PluginSessions
Create and manage Claude Code sessions programmatically:
// Create a new session on your plugin's own virtual project.
// There is no `projectId` option -- the host always derives the project from
// your plugin ID. See the Sessions API reference.
const { sessionId } = await ctx.sessions.create({
prompt: 'Analyze the codebase for security issues',
userInitiated: true,
})
// Interact with the session
await ctx.sessions.sendMessage(sessionId, 'Focus on SQL injection')
const status = await ctx.sessions.getStatus(sessionId)
const messages = await ctx.sessions.getMessages(sessionId)
// Monitor status changes
const unsubscribe = ctx.sessions.onStatusChange(sessionId, (status) => {
ctx.log.info(`Session ${sessionId} is now: ${status}`)
})
// Stop the session
await ctx.sessions.stop(sessionId)ai
Grants access to: PluginAi
Call AI models directly for text generation without creating a full session:
// Generate a response with a system prompt and user prompt
const response = await ctx.ai.generateMessage(
'You are a helpful code reviewer.',
'Review this function for potential bugs: ...',
)
// Generate a short title from text
const title = await ctx.ai.generateTitle(
'This PR adds pagination to the user list endpoint...',
)network
Grants access to: PluginHttp
Make outbound HTTP requests:
const response = await ctx.http.fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'test' }),
})
const data = await response.json()The fetch API follows the standard Fetch API interface.
cron
Grants access to: PluginCron
Register and manage scheduled tasks:
// Register a handler for a cron job declared in manifest.json
ctx.cron.register('heartbeat', '*/30 * * * *', async () => {
ctx.log.info('Running heartbeat check')
const status = await checkHealth()
await ctx.db.insert('checks', { status, timestamp: Date.now() })
})
// Check if a job is registered
const active = ctx.cron.isRegistered('heartbeat')
// Unregister a job
ctx.cron.unregister('heartbeat')TIP
Cron jobs must also be declared in the cron.jobs array in manifest.json. The id you pass to register() must match a declared job ID. See Manifest > Cron Block.
cli
Grants access to: PluginCli
Register HTTP endpoint handlers accessible through AMC's CLI control server:
ctx.cli.handle('status', async (req) => {
// req: { method, path, body?, query? }
const checks = await ctx.db.query('checks', {
orderBy: { created_at: 'desc' },
limit: 5,
})
return {
status: 200,
body: { healthy: true, recentChecks: checks },
}
})
// Remove a handler
ctx.cli.removeHandler('status')Endpoints are reached at http://127.0.0.1:19519/plugins/<plugin-id>/<path>.
TIP
CLI endpoints must also be declared in the cli.endpoints array in manifest.json. The path you pass to handle() must match a declared endpoint path. See Manifest > CLI Block.
notifications
Grants access to: PluginToast.notify()
Send OS-level desktop notifications (system tray notifications):
ctx.toast.notify({
title: 'Build Complete',
body: 'Your project has been built successfully.',
})Note that ctx.toast.show() (in-app toasts) is available without this permission. The notifications permission is only required for ctx.toast.notify(), which triggers a native OS notification.
rss
Grants access to: AMC's built-in RSS feed data
Allows the plugin to read RSS feeds and articles managed by AMC's Channels system:
// Fetch articles from a specific feed
const articles = await ctx.rss.getArticles({ feedId: 'abc-123', limit: 20 })
// Get all configured feeds
const feeds = await ctx.rss.getFeeds()Plugins that source content from RSS feeds (e.g. newsletter builders, digest generators) should declare this permission.
system
Grants access to: host shell / clipboard / process capabilities exposed through the UI bridge (window.AgentMC)
Covers privileged desktop actions surfaced to a plugin's webview UI — opening a path or revealing an item in the OS file manager, reading text/images from the clipboard, and launching or signalling child processes. Only declare it if your plugin's UI genuinely drives the host desktop.
chrome
Grants access to: host chrome / navigation surfaces exposed through the UI bridge (toolbar items, context menus, in-app navigation)
Lets a plugin's UI integrate with AMC's own chrome — contributing toolbar/context-menu entries and navigating within the app shell.
inbox
Grants access to: PluginInbox
Surface items in AMC's inbox -- the unified list where the user reviews things that need their attention:
await ctx.inbox.setItems([
{ id: 'scan-report', title: 'Security scan finished', priority: 'high' },
])See the Inbox API for the full InboxItem shape.
auth
Grants access to: PluginAuth identity methods
See who is signed in to AMC (name and email) and react to sign-in state:
const user = await ctx.auth.getUser()
const signedIn = await ctx.auth.isAuthenticated()This covers getUser(), isAuthenticated(), getGoogleIdToken(), onAuthStateChange(), and requestSignIn(). See the Auth API.
auth.session
Grants access to: PluginAuth.getSession()
Request scoped OAuth access tokens for Google and GitHub on the user's behalf, so your plugin can call those providers' APIs as the user:
const session = await ctx.auth.getSession('google', [
'https://www.googleapis.com/auth/calendar.readonly',
])This is a distinct, higher-trust permission from auth. Declare it only if you need to act against a provider's API; declare network too if you call that API via ctx.http.fetch.
navigation
Grants: the ability to navigate AMC to your sessions, projects, and views
The navigation permission lets a plugin ask AMC to move the user to a session, project, or view. There is no ctx.navigation API in the current SDK surface -- navigation is host-gated and requested through the host bridge. Declare navigation so a plugin that drives AMC's navigation installs cleanly and is recognized at runtime.
recording
Grants access to: PluginRecording (screen-recording control)
Start and stop screen recordings and manage the resulting files.
Bridge pending
ctx.recording is fully wired and Tier-1 elevated: start() / stop() / list() / get() are real, every start raises a native confirm the plugin cannot bypass, and the host redacts file paths and share tokens from everything the plugin can see. (This section previously said the bridge was "not yet wired" and that calls were inert — that stopped being true before 2026-08-11.) There is no webview equivalent: AgentMC.recording does not exist. See the Recording API.
tts
Grants access to: PluginTts (text-to-speech)
Read text aloud using whichever voice the user configured in AMC. isAvailable() reports false when the user has TTS turned off or has set up no voice provider — check it before offering a read-aloud control.
Metered spend
Synthesis costs real money and is billed to the user. AMC enforces its own per-plugin daily cap (shared with the ai capability) and synthesize() rejects once that cap is hit. Treat the rejection as an expected runtime state, not a bug — surface it to the user rather than retrying.
See the Text to Speech API.
sessions.readHistory
Grants access to: PluginSessionHistory (read past sessions and projects)
Read the user's existing AMC sessions and projects — distinct from sessions, which creates and drives new ones. Intended for plugins that turn prior work into a summary, a deck, or a report.
This is the most privacy-sensitive permission in the SDK, and it is default-deny at runtime:
- The plugin sees nothing until it calls
requestAccess(), which opens a picker where the user chooses exactly which projects or sessions to hand over.getMessages()on anything else throws. - Content is text only. Tool calls, tool output, and file contents are stripped by the host before the plugin sees them, so secrets embedded in tool blocks never travel.
- Every read is written to an audit log the plugin cannot touch, and the user can revoke the grant at any time from Settings → Plugins.
Handle a cancelled grant (result.cancelled === true) as a normal outcome — declining is the expected default.
See the Session History API.
firebase
Grants access to: PluginFirebase (enumerate Firebase accounts and projects)
List the Firebase accounts the user is signed into, list their projects, and start an interactive firebase login. Backed by the user's locally installed Firebase CLI.
Empty, never thrown
Every list method resolves to an empty array when the CLI is missing, times out, or returns something unparseable — none of them reject. So an empty result does not mean "no projects". Call setupStatus() to tell a genuinely empty account apart from a machine with no Firebase CLI, and branch on cliInstalled / signedIn before showing an empty state.
See the Firebase API.
spend
Grants access to: PluginSpend (read-only AI cost and usage totals)
Read the user's AI spend breakdown — headline totals for yesterday, the last 7 days, and the last 30 days, plus per-engine coding lines, per-feature background lines, and notable individual charges. This is what a spend-report plugin is built on.
Read-only, and there is nothing to pass: the host resolves the time windows and the user's timezone itself, so there is no window to widen. Note that the numbers are the user's global spend across all their accounts, not a slice scoped to your plugin.
codingValue is a shadow figure — what the agent-coding work would have cost at API rates, though it is covered by the flat plan. outOfPocket is the money actually spent. Do not present the two as the same thing.
See the Spend API.
Declaring Permissions
Add permissions to the permissions array in manifest.json:
{
"permissions": ["storage", "sessions", "ai", "network"]
}Only request the permissions your plugin actually needs. Users see the permission list during installation, and requesting unnecessary permissions may discourage adoption.
Summary Table
| Permission | APIs Granted | Use Case |
|---|---|---|
storage | PluginStorage, PluginDb, PluginFs | Persist data, query collections, read/write files |
secrets | PluginSecrets | Store credentials encrypted by the OS keychain |
sessions | PluginSessions | Create/manage Claude Code sessions |
sessions.readHistory | PluginSessionHistory (webview) | Read PAST sessions/projects the user explicitly grants |
ai | PluginAi | Direct AI text generation |
tts | PluginTts (webview, AgentMC.tts) | Read text aloud with the user's configured voice (metered) |
network | PluginHttp | Outbound HTTP requests |
cron | PluginCron | Scheduled background tasks |
cli | PluginCli | HTTP endpoints on AMC's control server |
notifications | PluginToast.notify() | Native OS desktop notifications |
system | Shell / clipboard / process (webview UI bridge) | Open paths, read clipboard, launch host processes from your plugin's UI — never a licence for the backend to import child_process |
rss | PluginRss | Read RSS feeds and articles from AMC's Channels |
auth | PluginAuth (identity) | See who is signed in; react to sign-in state |
auth.session | PluginAuth.getSession() | Scoped Google/GitHub access tokens on the user's behalf |
chrome | Toolbar / context-menu / navigation (UI bridge) | Contribute chrome and navigate the app shell |
firebase | PluginFirebase (webview) | List the user's Firebase accounts/projects; start a login |
recording | PluginRecording | Screen recording, host-mediated with a per-start confirm (backend only) |
inbox | PluginInbox.setItems() | Contribute rows to the AMC inbox |
navigation | (host-gated; no ctx API) | Navigate AMC to sessions, projects, and views |
spend | PluginSpend | Read-only AI cost/usage totals for spend reports |
workspace.read | WorkspaceApi (read half) | Enumerate and read the user's real project files and worktrees |
workspace.write | WorkspaceApi.writeFile / writeFiles / mkdir / deleteFile | Write to the user's real project files (does NOT imply workspace.read — declare both) |
workspace.exec | WorkspaceApi.run | Run one bounded command in a granted project, behind a native confirm |
| (none) | PluginSettings, PluginLogger, PluginEvents, PluginSidebar, PluginToast.show() | Always available |
workspace.* is real, and the three do not imply one another
This section previously said the host had no workspace runtime. It has had one since 2026-08-05: fourteen backend methods, with workspace.write and workspace.exec gating real mutating and command-running calls.
A manifest asking for workspace.write or workspace.exec without an explicit workspace.read is rejected — by the host loader, by the marketplace publish gate, and by amc-plugin validate. The host refuses to infer it so the consent card the user reads matches what the plugin actually holds. See the Workspace API.
Three permissions grant a WEBVIEW namespace, not a backend one
tts, sessions.readHistory and firebase gate AgentMC.tts / AgentMC.sessionHistory / AgentMC.firebase. The host builds no backend entry for any of them, so ctx.tts and friends are undefined — a TypeError at activation, not a permission error. The table above lists the type each grants; the surface is the webview.