- Keep users’ own mods out, with or without mods of your own: Stop user-installed mods from loading
- See what your users get when you change nothing: Know what happens by default
- Leave mods on with other limits: Choose how much to allow
These cases are covered on other pages:
- You haven’t deployed managed settings before: start with Deploy managed settings
- You want to control which plugins users can install: see Manage plugins for your organization
Stop user-installed mods from loading
To keep every mod your users bring from loading, set theallowManagedModsOnly option on the built-in guard, a policy mod that Claude Code loads ahead of every mod a user installs. The option goes in managed settings under pluginConfigs, keyed by cc-plugin-sec-default@builtin:
managed-settings.json
- No mod a user brings loads: that covers a mod in a plugin the user installed, a mod loaded with
--plugin-dir, and a mod Claude wrote during a session - Your organization’s mods still load: a mod that counts as your organization’s isn’t checked. Every other mod counts as a user’s and doesn’t load. That includes a mod in a plugin you enable from a GitHub or other remote marketplace, and one your organization turns on for its members on claude.ai. If none counts as yours, no installed mod loads.
- Users can’t undo it: the guard reads the option from managed settings only, so the same entry in a user, project, or local settings file, or in a file passed with
--settings, changes nothing - A file or MDM policy covers every provider: when you deliver the option as a file or through MDM, it works the same way on Amazon Bedrock, Google Cloud’s Agent Platform, and Microsoft Foundry. For delivery from the claude.ai admin console, see Platform availability
- Users’ other customizations keep working: their hooks in settings files, status lines, and
/goalaren’t affected - Built-in mods keep running: mods built into Claude Code, such as
AGENTS.mdsupport, each have their own switch
--plugin-dir and the path of a directory that holds a mod, such as claude --plugin-dir ./first-mod. The mod’s hooks don’t run, and the transcript and the debug log have the guard’s message, which names the mod and allowManagedModsOnly. If the mod loads, see Check that a policy is in force and the rules that decide whether an option takes effect.
If you set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS to 0 during early access, replace it with this option. Claude Code v2.1.287 and later ignores the variable at any value, so a 0 there leaves mods on.
Know what happens by default
With no mod settings of your own, this is what your users get:-
Mods are on. A user can install a plugin that contains a mod from any marketplace your plugin settings allow, or load one from a directory with
--plugin-dir. -
A built-in guard runs first. Claude Code loads a built-in mod named
sec-default@builtinahead of every mod a user installs. Users can’t turn it off./pluginand the debug log list it ascc-plugin-sec-default. The guard loads when either of these is true:- The machine has managed settings
- The user is signed in to Claude Code with a Team or Enterprise plan
-
The guard protects what you manage. A user’s mod can’t change what your managed hooks receive or decide, the system prompt, your managed
CLAUDE.mdand other managed instructions, what any mod reads as settings, or the tools and descriptions of your managed MCP servers. - Everything else is allowed. The guard adds no other restrictions. A user’s mod can still read and write files, start processes, make network requests, rewrite tool calls and prompts, deny a tool call, approve one that would otherwise prompt, and draw in the interface, all with that user’s permissions.
-
Deny rules and your managed hooks take precedence. Where the guard loads, a user’s mod can’t approve a call that a
denyrule refuses, whichever settings file holds the rule. A block from aPreToolUsehook in managed settings is final too. Both apply to Claude’s tool calls. Neither applies to a mod’s own$.fsand$.processcalls: withRead(.env)denied, a mod can still read that file with$.fs.reador start a program that does. To limit those calls, keep the mod from loading or hook the call in a policy mod. -
Other permission checks can be overridden. A user’s mod that approves tool calls can approve a call that an
askrule would prompt for, or that aPreToolUsehook outside managed settings blocked. In auto mode, a call the mod approves runs without a classifier check.
mods/sec-default directory of the Claude Code repository.
Know which controls still apply
Mods don’t replace the controls you already have:- Settings hooks keep working. Command, HTTP, prompt, and agent hooks in settings files and in plugins’
hooks/hooks.jsonrun as before, alongside mods. Nothing about them is deprecated. - Deny rules take precedence where the guard loads. A user’s mod can’t approve a call that a
denyrule refuses, unless you setallowModsToOverrideDenyRules. - Managed hooks run first. A
PreToolUsehook in managed settings runs before any mod sees the tool call, and its block is final. If a mod then rewrites the call, your managed hooks run again on the rewritten call, so a block still applies.PreToolUsehooks from other settings files and from plugins run after the last mod, so a mod that returns its own result in place of running the tool keeps those from running. See The order mods run in. - Network policy covers
$.http.fetch. If your organization turns off web fetching, or nonessential network traffic is turned off for the session, Claude Code refuses a network request that a mod makes with$.http.fetch. The policy doesn’t cover a program the mod starts with$.process.run. That program reaches the network with the user’s own access. - Plugin controls cover mods. A mod is a plugin, so the settings that restrict what users can install, such as
strictKnownMarketplaces, decide whether it can be installed at all. - Mods can’t change the permission prompt. A mod can restyle much of Claude Code’s interface, but not the permission prompt, so it can’t change what a prompt shows. A mod can still approve or deny a tool call before the prompt appears, as Know what happens by default describes.
- Trust prompts come first. In an interactive session in a directory the user hasn’t trusted yet, no mod loads until they answer the trust prompt.
--safe-modeturns installed mods off, yours included. Start a session withclaude --safe-modeto check whether a mod caused a problem.
Decide whether to leave mods on
A mod can do more than the other parts of a plugin because it runs inside Claude Code. It sees every prompt and tool call, can change them, and can allow or deny a tool call before a permission prompt appears. What a user can load as a mod depends on the plugin controls you already have:
Manage plugins for your organization lists every way a plugin loads and the setting that controls each.
To check the mods in a marketplace before your users install them, see Review what a mod can do. To keep users’ mods out until you’ve done that, see Stop user-installed mods from loading.
Review what a mod can do
You can see what a mod is able to do without running it. In your shell, runclaude plugin validate on the plugin’s directory:
hooks: line lists the events the mod receives. The calls: line lists the mods API methods its code calls. The mods API, written $ in a mod’s code, is how a mod reaches files, processes, and the network. Claude Code refuses to load a mod that uses the mods API in a way this command can’t read.
Look at the calls: line for these:
In the
hooks: line, tool.call and prompt.submit mean the mod sees every tool call and every prompt, and can change them. session.append means the mod can rewrite each row of the conversation before it’s stored. ui.render{component=AskUserQuestion} means the mod can redraw the dialog Claude uses to ask the user a question. tool.check means the mod can approve or deny a tool call before a permission prompt appears. Know what happens by default lists which of your rules and hooks take precedence over its answer.
Choose how much to allow
Mod policies range from no installed mods at all to any mod a user chooses, with your own mod checking the others, and each one is a few managed settings. Find the policy you want in the first column and set what the second column names. Deploy managed settings covers where managed settings live.
What each setting does:
allowManagedModsOnly: an option on the built-in guard. Users’ own mods don’t load, and their settings hooks, status lines, and/goalkeep working. Stop user-installed mods from loading lists what it covers.allowManagedHooksOnly: a wider setting. Only your organization’s mods and the mods built into Claude Code load. A mod a user installed themselves doesn’t. The setting also blocks hooks in users’ own settings files. Read What runs underallowManagedHooksOnlybefore you set it.disableAllHooks: the widest setting. In managed settings, it stops the mods in every installed plugin, yours included, and turns off every hook in settings files, so aPreToolUsehook in your managed settings no longer blocks anything. Custom status lines and/goalstop working too. ReaddisableAllHooksbefore you set it.disableSideloadFlags: rejects--plugin-dirand--plugin-urlat startup, so nobody loads a mod from a directory, and keeps mods Claude writes during a session from loading. The setting also rejects--agentsand--mcp-config. ReaddisableSideloadFlagsbefore you set it.
AGENTS.md support, aren’t affected by these settings. Each has its own switch.
A user whose mod didn’t load finds the reason in their debug log. Refusal messages lists the lines for allowManagedHooksOnly and disableAllHooks, and Messages from the built-in guard has the line for allowManagedModsOnly.
Set options on the built-in guard
The built-in guard takes two options. Set them in managed settings underpluginConfigs, keyed by cc-plugin-sec-default@builtin, as the example in Stop user-installed mods from loading does.
The table gives what your users get with each option unset and with it set to true:
These rules decide whether an option takes effect:
- The id has one spelling here: Claude Code reads the options only under
cc-plugin-sec-default@builtin.prependPluginsacceptssec-default@builtinas well, andpluginConfigsdoesn’t. - Only managed settings count: the same entry in a user, project, or local settings file, or in a file passed with
--settings, neither sets an option nor loosens one - The guard has to load: if you set
prependPlugins, name the guard in the list. Where the guard doesn’t load, neither option applies. - The guard fails closed: if the guard can’t read managed settings, it refuses every user’s mod at load. If it can’t check the deny rules for a call that a user’s mod approved, it refuses the call.
Run your organization’s own mods
You can deploy mods of your own to every user, choose where they run relative to users’ mods, and use one to enforce a policy.Install your organization’s mods and set the order
Your organization’s mods load where users’ mods don’t and can run ahead of them, so Claude Code has to be able to tell that a mod came from you. It treats a mod as your organization’s only when all of these are true:- Managed
enabledPluginssets the mod’s plugin totrue - Managed settings name the plugin’s marketplace as a directory on the user’s machine, by absolute path. An
extraKnownMarketplacesentry does that and registers the marketplace for the user too. - The marketplace lists the plugin by a relative path, so Claude Code loads it in place from that directory
/opt/acme/claude-plugins/.claude-plugin/marketplace.json
enabledPlugins enables it. That covers every plugin from a GitHub, git, URL, or npm source. Its mod runs among users’ mods, prependPlugins and appendPlugins skip it, and it doesn’t load under allowManagedModsOnly or allowManagedHooksOnly. The user’s debug log has a line that starts with the plugin’s id and is enabled by managed settings, but.
Claude Code raises an event each time it’s about to act, such as run a tool, and passes it to each mod in turn. A mod that counts as yours runs before users’ mods even when you list it nowhere. To set its place, list its id in one of two settings. The id is the plugin’s name, @, and the marketplace’s name, such as acme-guard@acme-tools.
prependPlugins: your mod sees every event before any user’s mod and every result after. It can change the event, refuse it, or skip the users’ mods.appendPlugins: your mod runs after every user’s mod, so it sees only the events those mods pass on, in the form they pass them
acme-tools marketplace at /opt/acme/claude-plugins, enables acme-guard from it, and runs that mod first, with the built-in guard after it:
managed-settings.json
extraKnownMarketplaces: names the directory that holds theacme-toolsmarketplace.pathis the absolute path of the directory that contains.claude-plugin/marketplace.json.enabledPlugins: turnsacme-guardon for every user who receives these managed settingsprependPlugins: putsacme-guardfirst and the built-in guard second, both ahead of any mod a user installs. Claude Code follows the order you list.
claude --debug and search the debug log for the mod’s id:
hooks module acme-guard@acme-tools loaded, withtier prepend: the mod counts as your organization’s and runs first- The same line with
tier user: Claude Code treats it as a user’s mod. A second line,prependPlugins names acme-guard@acme-tools, which is not an enabled managed plugin with a hooks module; skipped, says the list skipped it.
- The list replaces the default: when you set
prependPluginsin managed settings, namesec-default@builtinin it to keep the built-in guard. The guard is built in and needs noenabledPluginsentry. - Your own ids must count as yours: in managed settings, Claude Code skips an id whose plugin doesn’t meet the three conditions for an organization’s mod
- Repositories can’t set them: Claude Code reads both settings from managed settings and never from a repository’s settings file. A user can set them in
~/.claude/settings.jsonto order their own mods only on a machine with no managed settings, and only when they aren’t signed in with a Team or Enterprise plan. Anywhere else, Claude Code ignores both keys in user settings. A list there neither adds nor removes the built-in guard.
Enforce a policy with a mod of your own
To keep every user’s mod out, you don’t need a mod of your own. SetallowManagedModsOnly. Write a policy mod when you want to admit some users’ mods and refuse others, or to record what mods do.
Each time another mod is about to load, your mod receives the list that claude plugin validate prints, in an event named plugin.register. A mod in prependPlugins can read that list and refuse the mod. It can also hook any mods API call by name to record or refuse that call for every other mod. The name is the method without the $., so a hook on fs.write sees every $.fs.write call.
This policy mod refuses any user’s mod whose own code calls $.process.run or $.process.spawn. It also keeps an audit log, writing each tool call and each file a mod writes to the debug log. Because it runs first, the log records what was requested, before any user’s mod changes it. Save it as acme-guard/hooks/register.js:
acme-guard/hooks/register.js
plugin.register: decides whether another mod loads. It refuses a user’s mod that calls a blocked method and passes every other mod on.tool.call: writes a line such asaudit tool.call Bashto the debug log for each tool call, and changes nothingfs.write: writes a line such asaudit fs.write by reader "/tmp/notes.md"for each$.fs.writecall another mod makes, and changes nothing. The mod’s name comes first and the path is quoted, so a path that a mod picks can’t pass for another field of the line.
plugin.register hook reads two fields of the event:
e.tier: where the mod would run, one ofprepend,user,append, orbuiltin. Every mod a person installs isuser.e.uses.calls: the mods API methods the mod calls, each spellednamespace.methodsuch asprocess.run, without the$.thatclaude plugin validateprints
$.process.run, the mod doesn’t load, and their debug log has a line that ends with refused by acme-guard: and your reason. The refusal also reaches the transcript in a session that hot-reloads a plugin directory. To block a call without refusing the whole mod, return { deny: 'your reason' } from a hook on that call’s name.
To send the audit lines somewhere other than the debug log, call $.http.fetch from the same hooks.
A session can run without your mod. If the worker thread that runs installed mods crashes three times, Claude Code unloads every mod that isn’t built in, including yours, until the user runs /reload-plugins or starts a new session. And a user who starts Claude Code with --safe-mode runs without installed mods, yours included.
Create a mod covers the files a mod needs. Test a mod that judges other mods has a test file for this policy mod.
Refuse mods when your check fails
If yourplugin.register hook throws or runs past its time limit, Claude Code skips the hook, so the check fails open and the mod it was checking loads. To fail closed and refuse users’ mods, move the check into a named function and add a .catch handler that returns the refusal. This version of the file shows the plugin.register hook only, so keep the two audit hooks from the first version in register:
acme-guard/hooks/register.js
refused by acme-guard: Acme policy check failed, so this mod was not loaded. The handler passes every mod outside the user tier to next(e), so a failed check doesn’t stop the mods your organization lists. Handle a hook that fails covers .catch for other events.
Next steps
- Plugin security: what any plugin can do on a user’s machine, and how to review one before it’s installed
- Mods overview: what a mod is and how it compares to hooks, skills, and MCP servers
- The order mods run in: how
prependPluginsandappendPluginsfit with users’ mods - Settings and environment variables: every setting named on this page in one table