Skip to content

Manage mods for your organisation

Decide whether mods run on your fleet, allow only your own, review what a mod can do, and enforce policy with a mod you control.

A mod is a plugin that runs JavaScript inside Claude Code with the full permissions of the person who installed it. There is no sandbox. That makes mods powerful and also makes them something an administrator should think about deliberately. Using managed settings you can switch user mods off, allow only mods you ship, set the order they run in, or deploy a mod of your own that inspects and vetoes the others.

This page is for whoever deploys managed settings, whether as a file, through MDM, or from the claude.ai admin console. Mods are enabled by default from Claude Code v2.1.286. If you have never deployed managed settings, read Managed settings first; for controlling which plugins can be installed at all, see Manage plugins for your organisation.

The quick answer: block user mods

If your security team's position is "no third-party code inside the agent", this is the whole job. Put the following in managed settings:

{
  "pluginConfigs": {
    "cc-plugin-sec-default@builtin": {
      "options": {
        "allowManagedModsOnly": true
      }
    }
  }
}

What that achieves:

  • No user-supplied mod runs its hooks. That includes mods in plugins the user installed, mods loaded with --plugin-dir, and mods Claude wrote during a session.
  • Mods that count as your organisation's still run. Everything else is treated as a user mod and refused, including mods in plugins you enabled from GitHub or another remote marketplace, and mods your organisation turned on through claude.ai. If nothing qualifies as yours, no installed mod runs at all.
  • Users cannot reverse it. The option is only read from managed settings. Copying it into user, project, local or --settings files changes nothing.
  • It works on every provider when delivered as a file or MDM policy, including Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry. Admin-console delivery has its own availability rules; see Server-managed settings.
  • Ordinary customisation is untouched. Hooks in settings files and in plugins' hooks/hooks.json, custom status lines and /goal keep working.
  • Built-in mods keep running. Features like AGENTS.md support are implemented as built-in mods, each with its own switch described on the mods overview.

To prove it on a machine, start Claude Code with a test mod sideloaded, for example claude --plugin-dir ~/scratch/hello-mod. The mod's hooks should not run, and both the transcript and the debug log should carry the built-in guard's message naming the mod and allowManagedModsOnly. If the message is missing, check the policy actually loaded (see Managed settings) and the rules for guard options.

Note: If you used CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=0 during the early-access period, swap it for the option above. From v2.1.287 that variable is ignored whatever its value, so leaving it in place means mods are on.

What happens if you do nothing

With no mod-specific policy:

  • Mods are on. Users can install mod-bearing plugins from any marketplace your plugin policy allows, or sideload one with --plugin-dir.
  • A built-in guard loads first. A built-in mod called sec-default@builtin (shown as cc-plugin-sec-default in /plugin and the debug log) loads ahead of all user mods and cannot be disabled by users. It loads when the machine has managed settings, or when the user is signed in with a Team or Enterprise plan. API-key users and users on Bedrock, Agent Platform or Foundry only get it on machines with managed settings.
  • The guard protects your managed configuration. A user's mod cannot alter what your managed hooks receive or decide, the system prompt, managed CLAUDE.md and other managed instructions, what any mod reads as settings, or the tools and descriptions of managed MCP servers.
  • Beyond that, anything goes. User mods can read and write files, start processes, make network requests, rewrite prompts and tool calls, deny a tool call, approve one that would have prompted, and draw UI, all as the user.
  • Deny rules and managed hooks still win. Where the guard is loaded, a user's mod cannot approve a tool call that any deny rule refuses, and a block from a managed PreToolUse hook is final. Both apply to Claude's tool calls only. They do not cover a mod's own $.fs and $.process calls: with Read(.env) denied, a mod can still read .env through $.fs.read or by launching a program. To control that, stop the mod loading or intercept the call in a policy mod.
  • Softer checks can be bypassed. A user's mod can approve a call that an ask rule would have prompted for, or that a non-managed PreToolUse hook blocked. In auto mode, a mod-approved call skips the classifier.

The guard's source is public in the mods/sec-default directory of the Claude Code repository on GitHub.

Controls that still apply alongside mods

  • Settings hooks. Command, HTTP, prompt and agent hooks (see Hooks) keep running next to mods and are not deprecated.
  • Deny rules, unless you set allowModsToOverrideDenyRules.
  • Managed hooks run first. A managed PreToolUse hook sees the tool call before any mod and its block is final. If a mod rewrites the call, managed hooks run again on the new version. Non-managed and plugin PreToolUse hooks run after the last mod, so a mod that supplies its own result instead of running the tool prevents those from firing. Mod events shows the ordering.
  • Network policy covers $.http.fetch. If web fetching is disabled for the organisation, or non-essential traffic is off, mod fetches are refused. Programs started via $.process.run are not covered and use the user's own network access.
  • Plugin install controls. A mod is a plugin, so strictKnownMarketplaces and friends decide whether it can be installed.
  • The permission prompt is off-limits. Mods can restyle a lot of the interface but not the permission prompt, so they cannot misrepresent what it shows. They can still answer a tool call before the prompt appears.
  • Trust comes first. In an untrusted directory, no mod loads until the user answers the trust prompt.
  • Safe mode. claude --safe-mode starts without any installed mods (yours included), which is useful for ruling a mod in or out when debugging.

None of this is a sandbox. An allowed mod runs as the user.

Deciding whether to leave mods on

Mods can do more than other plugin components because they run inside the agent loop. They see every prompt and tool call, can modify both, and can answer permission questions before the user is asked.

What a user can actually load depends on your existing plugin policy:

Current plugin policyMods a user can load
NothingFrom any marketplace, any directory via --plugin-dir, or written by Claude mid-session
Marketplace allowlistFrom allowed marketplaces or any directory via --plugin-dir. Claude-written mods only if the allowlist includes skills-dir
Allowlist plus disableSideloadFlagsOnly from allowed marketplaces

Reviewing a mod before approving it

You can see what a mod is capable of without running it. Point the validator at the plugin folder:

claude plugin validate ./vendor-mods/session-recorder

Two lines in the output matter. hooks: lists the events the mod subscribes to, and calls: lists the mods API ($) methods its code uses. For example:

  ❯ ./register.js hooks: session.start, prompt.submit, ui.render{component=Pane}
  ❯ ./register.js calls: $.fs.write, $.http.fetch, $.env.get

Claude Code refuses to load any mod whose use of the API the validator cannot analyse, so this list is reliable.

How to read the calls: line:

CallRisk to consider
$.fs.read, $.fs.writeReads or writes any file the user can reach
$.process.run, $.process.spawnLaunches programs as the user
$.http.fetchMakes network requests
$.env.get, $.settings.readReads environment variables and settings, which may contain keys. An env reads: line names each variable
$.env.setSets a variable for Claude Code and every command and MCP server it later starts, which can change their behaviour. An env writes: line names each one
$.mcp.callCalls a tool on a connected MCP server, subject to session permissions
$.model.completeSpends the user's plan or API key on model calls
$.prompt.submitSubmits prompts, potentially as if the user typed them
$.session.sendSends a message that another session's or subagent's Claude will read

And the hooks: line:

  • tool.call and prompt.submit mean the mod sees, and can alter, every tool call and every prompt.
  • session.append lets it rewrite each conversation row before storage.
  • ui.render{component=AskUserQuestion} lets it redraw the dialog Claude uses to ask the user a question.
  • tool.check lets it approve or deny a tool call before any prompt, subject to the precedence rules above.

The mods reference and mods API describe each event and method.

Policy recipes

Every policy is a handful of managed keys:

GoalWhat to set
No installed mods, settings hooks unaffectedallowManagedModsOnly and ship no mods of your own
No installed mods and no hooks whatsoever, including managed onesdisableAllHooks: true
Only your organisation's modsallowManagedModsOnly, plus install your mods so they count as yours
Any mod from marketplaces you approveYour marketplace restrictions plus disableSideloadFlags: true
Any mod, but yours checks the restInstall your mod and list it with sec-default@builtin in prependPlugins

What the keys do, from narrowest to widest:

  • allowManagedModsOnly (a guard option): refuses users' mods; their settings hooks, status lines and /goal carry on.
  • disableSideloadFlags: rejects --plugin-dir and --plugin-url at start-up and stops Claude-written mods loading. It also rejects --agents and --mcp-config.
  • allowManagedHooksOnly: only your organisation's mods and built-in mods load, and hooks in users' own settings files are blocked too. Read its entry in the settings reference first.
  • disableAllHooks: in managed settings, stops mods in every installed plugin including yours and disables all settings hooks, so even a managed PreToolUse block stops working. Custom status lines and /goal stop too.

None of these affect built-in mods. Users whose mod was refused will find the reason in their debug log; the exact lines are listed in Troubleshoot mods.

Recipe: only your organisation's mods

This is a complete managed-settings.json that ships one company mod, runs it first with the guard after it, refuses user mods and blocks sideloading:

{
  "extraKnownMarketplaces": {
    "northwind-internal": {
      "source": { "source": "directory", "path": "/Library/Northwind/claude-marketplace" }
    }
  },
  "enabledPlugins": { "nw-policy@northwind-internal": true },
  "prependPlugins": ["nw-policy@northwind-internal", "sec-default@builtin"],
  "pluginConfigs": {
    "cc-plugin-sec-default@builtin": {
      "options": { "allowManagedModsOnly": true }
    }
  },
  "disableSideloadFlags": true
}

The first three keys install your mod so it counts as yours and set the order. pluginConfigs switches on the guard option. disableSideloadFlags closes the --plugin-dir door.

Verify on a test machine with claude --debug and look for:

  • your mod's hooks module line showing tier prepend;
  • for any user-installed mod, a line reading refused by cc-plugin-sec-default: mods are limited to your organization's by policy (allowManagedModsOnly). An earlier line will say its hooks module loaded, so search specifically for the refusal;
  • claude --plugin-dir ./anything exiting with a message beginning --plugin-dir is disabled by your organization's managed settings (disableSideloadFlags).

Combine with marketplace restrictions if you also want to limit which marketplaces users can add.

Your plugin controls apply to mods too

Because a mod is a plugin, everything on Manage plugins for your organisation applies: auditing installs, setting update policy, per-group policy via separate endpoint settings, which surfaces apply the keys, and seeding containers. To offer optional mods, host them in a marketplace (Host a marketplace), bearing in mind that anything copied from a GitHub, git, URL or npm source counts as a user mod.

Options on the built-in guard

Set these under pluginConfigs → cc-plugin-sec-default@builtin → options in managed settings:

OptionWhen unsetWhen true
allowManagedModsOnlyUser mods runOnly your organisation's mods and built-in mods run hooks; every other mod, including --plugin-dir ones, is refused
allowModsToOverrideDenyRulesDeny rules beat user modsA user mod that approves tool calls may approve one a deny rule refuses

Rules that decide whether an option takes effect:

  • Exact id. Options are only read under cc-plugin-sec-default@builtin. prependPlugins also accepts the short form sec-default@builtin, but pluginConfigs does not.
  • Managed only. The same entry anywhere else neither sets nor loosens an option.
  • The guard must load. If you set prependPlugins, include the guard in it, otherwise neither option applies.
  • It fails closed. If the guard cannot read managed settings it refuses every user mod. If it cannot evaluate deny rules for a call a user mod approved, it refuses the call.

Shipping your own mods

What makes a mod "yours"

Organisation mods can run where user mods cannot, and ahead of them, so Claude Code needs proof of origin. A mod counts as your organisation's only if all three hold:

  1. Managed enabledPlugins sets its plugin to true.
  2. Managed settings name the plugin's marketplace as a directory on the local machine by absolute path (an extraKnownMarketplaces entry does this and registers it).
  3. The marketplace lists the plugin by relative path, so it loads in place rather than being copied. How plugins load explains in-place versus copied plugins.

In practice, have your device management tooling drop the marketplace folder at the same path on every machine, and make that folder and every parent writable only by administrators, just like the managed settings file. Anyone who can write there can rewrite your mod. Admin-console settings can carry the keys but cannot put the folder on disk.

A typical layout:

/Library/Northwind/claude-marketplace/
├── .claude-plugin/
│   └── marketplace.json
└── plugins/
    └── nw-policy/
        ├── .claude-plugin/
        │   └── plugin.json
        └── hooks/
            ├── hooks.json
            └── register.js
{
  "name": "northwind-internal",
  "owner": { "name": "Northwind IT" },
  "plugins": [
    { "name": "nw-policy", "source": "./plugins/nw-policy", "description": "Northwind mod policy and audit trail" }
  ]
}

Anything Claude Code copies into its cache counts as a user's mod even when managed enabledPlugins enables it. That is every plugin from GitHub, git, URL or npm sources. Such a mod runs among user mods, is skipped by prependPlugins and appendPlugins, is refused by allowManagedModsOnly, and does not load under allowManagedHooksOnly. The debug log shows a line starting with its id and is enabled by managed settings, but.

Ordering

Claude Code passes each event to every mod in turn. An organisation mod runs before user mods even if you list it nowhere. To control placement precisely, put its id (plugin@marketplace) in one of:

  • prependPlugins: sees every event before any user mod and every result after. It can modify an event, refuse it, or skip user mods entirely.
  • appendPlugins: runs after all user mods and sees only what they pass on, in the form they pass it.

Rules for these lists:

  • Setting prependPlugins in managed settings replaces the default order, so include sec-default@builtin to keep the guard. It needs no enabledPlugins entry.
  • Ids in managed lists that do not meet the "yours" conditions are skipped.
  • Repository settings can never set them. A user may set them in ~/.claude/settings.json to order their own mods only on a machine with no managed settings and when not signed in on Team or Enterprise. Elsewhere, user-level lists are ignored and cannot add or remove the guard.

To confirm placement, run claude --debug and search the log for your id. hooks module nw-policy@northwind-internal loaded with tier prepend means it is recognised as yours and runs first. The same line with tier user, followed by prependPlugins names nw-policy@northwind-internal, which is not an enabled managed plugin with a hooks module; skipped, means one of the three conditions failed.

Writing a policy mod

You do not need your own mod just to keep user mods out; allowManagedModsOnly does that. Write one when you want nuance: allow some user mods but not others, or keep an audit trail.

Two mechanisms make this possible. Whenever another mod is about to load, a prepended mod receives a plugin.register event carrying the same capability list the validator prints, and can refuse it. It can also hook any mods API method by name (the method without the $.), so a hook on http.fetch sees every other mod's $.http.fetch.

This example refuses user mods that use network access or set environment variables, and records every program another mod launches:

// nw-policy/hooks/register.js
const FORBIDDEN = new Set(['http.fetch', 'env.set'])

export function register(on) {
  on('plugin.register', async ($, e, next) => {
    if (e.tier !== 'user') return next(e)
    const hits = e.uses.calls.filter((name) => FORBIDDEN.has(name))
    if (hits.length) {
      return { refuse: `Northwind policy forbids ${hits.join(' and ')} in third-party mods` }
    }
    return next(e)
  })

  on('process.run', async ($, e, next) => {
    $.ui.log(`audit process.run from ${JSON.stringify(next.origin.plugin)}`, { to: 'debug' })
    return next(e)
  })
}

The fields used:

  • e.tier is where the candidate mod would run: prepend, user, append or builtin. Anything a person installs is user.
  • e.uses.calls lists the API methods it uses, written namespace.method without $..
  • next.origin.plugin identifies which mod made an intercepted API call. Quoting it with JSON.stringify keeps a mod-chosen value from masquerading as another field in your log line.

A refused mod does not load, and the user's debug log ends a line with refused by nw-policy: followed by your reason. In a session that hot-reloads a plugin directory the refusal also appears in the transcript. To block a single call without refusing the whole mod, return { deny: 'reason' } from a hook on that call's name. To ship audit lines somewhere central, call $.http.fetch from your own hooks (your mod is not subject to your own http.fetch rule, since it checks e.tier).

Be aware of two ways a session can end up without your mod: if the worker that runs installed mods crashes three times, every non-built-in mod (yours included) is unloaded until /reload-plugins or a new session; and --safe-mode skips installed mods entirely. See Troubleshoot mods.

Failing closed

If a plugin.register hook throws or exceeds its time limit, Claude Code skips it, which means the mod being checked loads. That is fail-open. To fail closed, register the check as a named function and attach .catch:

async function vetMod($, e, next) {
  const hits = e.uses.calls.filter((name) => FORBIDDEN.has(name))
  if (e.tier === 'user' && hits.length) {
    return { refuse: `Northwind policy forbids ${hits.join(' and ')} in third-party mods` }
  }
  return next(e)
}

export function register(on) {
  on('plugin.register', vetMod).catch(async ($, e, next) => {
    if (e.tier !== 'user') return next(e)
    return { refuse: 'Northwind policy check errored, so this mod was held back' }
  })
  // keep the process.run audit hook here as well
}

When the check fails, the user mod is refused with the fallback reason, while organisation and built-in mods still pass through. Mod events covers .catch on other events, Create a mod covers the files a mod needs, and Test a mod shows how to test one that judges other mods.