{"page":"lifecycle","title":"Tiers, order and next()","blurb":"The five tiers, how a hook wraps the ones below it, and everything a hook can do with next().","build":"2.1.286","sections":["The five tiers","next","The two axioms","next.to","next.origin","next.trace","next.signal and next.budget","What failure actually does",".catch","Serial, and why","Streaming","Recursion"],"limit":8000,"section":null,"offset":0,"count":7993,"total":26476,"next_offset":7993,"next_url":"https:\/\/changelogs.core-directive.com\/reference\/mods\/lifecycle.json?offset=7993","markdown":"Every hook on an event gets composed into one function, and that composition is a fold. The cheat sheet draws it three different ways because people keep reading it as a filter chain, which it isn't:\n\n```text\nX = A \u2218 B \u2218 C \u2218 core = A(B(C(core(\u22a5))))\n```\n\nA is outermost. It gets the event first and whatever it returns is the answer. Everything beneath it, core included, only runs because A called `next`. On the way down each hook can change the question, core answers it, and on the way back up each hook can change the answer. Whatever registered earlier wraps more, so position is authority.\n\n## The five tiers\n\nPlugins are ordered by tier, and the declarations put the rule in one sentence: \"outermost first: earlier is outer is more authority, and same-event hooks nest in this order and no other way.\"\n\n| Tier | Who | Position |\n|---|---|---|\n| `prepend` | Organisation policy, first | Outermost, wraps everything |\n| `user` | Everything a person installs | Middle |\n| `append` | Organisation policy, last | Beneath every user plugin |\n| `builtin` | Anthropic's own mods | Beneath that |\n| `core` | The engine | Innermost, does the thing |\n\n`prepend` and `append` are the organisation's. A mod an administrator lists in `prependPlugins` or `appendPlugins` lands there, and anything you install lands in `user`. Anthropic's admin page adds a case I'd missed. A mod that counts as the organisation's runs in `prepend` even when neither list names it ([their admin page](https:\/\/code.claude.com\/docs\/en\/plugins\/mods\/admin#install-your-organizations-mods), and [the order mods run in](https:\/\/code.claude.com\/docs\/en\/plugins\/mods\/events#the-order-mods-run-in)).\n\nOn a machine with managed settings, or on a Team or Enterprise plan, `sec-default` sits in `prepend` too, which is why a user plugin there can't touch classic hooks, prompt sections, settings reads, or the description of a tool the organisation provides. At 2.1.286 I had a user mod deny Bash from `classic.PreToolUse` on my managed machine, and the command ran anyway. The log said `classic.PreToolUse bypassed by cc-plugin-sec-default (tier user); beneath runs`.\n\nAnthropic's admin page says the guard protects what the organisation manages and \"adds no other restrictions\" ([know what happens by default](https:\/\/code.claude.com\/docs\/en\/plugins\/mods\/admin#know-what-happens-by-default)). At 2.1.287 it still skips every classic hook a user mod registers. I loaded a `--plugin-dir` mod whose `classic.Stop` hook writes a file and whose `classic.PreToolUse` hook writes a file and denies Bash, then had Haiku run `echo hi`. Neither file was written, the model reported \"The command printed: `hi`\", and the log said:\n\n```text\ncc-plugin-sec-default@builtin seated outermost: this machine has managed settings\nclassicprobe: classic.PreToolUse bypassed by cc-plugin-sec-default (tier user); beneath runs\nclassicprobe: classic.Stop bypassed by cc-plugin-sec-default (tier user); beneath runs\n```\n\nThat's one managed machine. I haven't run a user mod's classic hooks where the guard doesn't load.\n\nAn organisation that sets `prependPlugins` owns that tier and can list the mod in it or leave it out. Since 2.1.285 its name is `cc-plugin-sec-default@builtin`. The `prependPlugins` description in the 2.1.286 binary still says to list it as `sec-default@builtin`, \"the id every release reads\", for as long as any machine in the organisation may run a release from before the rename, and that a release that knows the new id reads either. I haven't tested either spelling in `prependPlugins`.\n\nAt 2.1.286 it also has two switches an organisation sets in managed settings, under `pluginConfigs[\"cc-plugin-sec-default@builtin\"].options`. With `allowManagedModsOnly` on, it refuses every `user` tier plugin at `plugin.register`, with `mods are limited to your organization's by policy (allowManagedModsOnly); <plugin> was not loaded`, and if its own check fails before deciding, the plugin is refused anyway. That check is the `judged by cc-plugin-sec-default: admitted` line in the [census on the anatomy page](\/reference\/mods\/anatomy): it's `sec-default` reading the policy and letting the plugin in. The other one works the opposite way round. The deny rules in the person's settings hold over installed plugins unless `allowModsToOverrideDenyRules` is `true`. When a `user` or `prepend` plugin's `tool.check` answer is looser than the one beneath it and a deny rule matches, the rule wins, and the person sees `<plugin> tried to lift a deny rule in your settings from a <tool> call (<rule>); the deny rule holds over the plugins you install (allowModsToOverrideDenyRules)`. If that check throws, a loosened answer becomes `the deny rules in your settings could not be checked for this call, so it is refused`. And with a managed `allowedMcpServers` list, a `user` plugin's `$.tool.register` gets `allowedMcpServers (managed): plugins outside policy may not add tools`. All of this paragraph is read from the 2.1.286 bundle, and I haven't triggered any of it.\n\nInside the `user` tier the order comes from dependencies, sorted so a plugin sits outside the ones it depends on. At 2.1.287 I passed `innerb` first and `outera`, which lists `innerb` under `dependencies`, second, and the log still put `outera` outside: `hooks module order: dependencies register cc-plugin-sec-default@builtin, outera@inline, innerb@inline, cc-plugin-agents-md@builtin, cc-plugin-telemetry@builtin`. There's no priority number and a hook can't move itself. Anthropic's position in the issue thread is that ordering belongs to the dependency graph, and a plugin doesn't get to decide how important it is.\n\nThe organisation's tiers are ordered by hand. Mods in `prependPlugins` and `appendPlugins` run in the order the list gives them ([Anthropic's settings table](https:\/\/code.claude.com\/docs\/en\/plugins\/mods\/reference#settings-and-environment-variables)). Inside one module, hooks on the same event run in the order `register` called `on`, and calling `on` twice for one event with no matcher fails the load with `on(\"session.start\") is registered twice without a matcher` ([filter which events a hook handles](https:\/\/code.claude.com\/docs\/en\/plugins\/mods\/events#filter-which-events-a-hook-handles)). I've taken both from Anthropic's pages and haven't run either.\n\n## next\n\n`next` is the rest of the chain below you, handed over as a function. The declarations describe it well enough to just quote:\n\n> The rest of the chain, as one hook receives it: made once per dispatch per hook, frozen\u2026 Each call runs the hooks below again; core is the last, and below it `next` rejects. Called with no argument it rejects, naming the hook. A hook that returns without calling it ends the chain; returning nothing is a failure.\n\nFour things follow from that, and three of them will bite you.\n\n**`next(e)` always needs an argument.** `next()` rejects. You pass the event down, changed or as you got it.\n\n**Calling it twice runs everything beneath twice.** That's supported on purpose. poteat in the thread: *\"completely supported; for example if you want to do a retry, backoff, etc.\"* On `tool.call` it means the tool runs twice.\n\n**Not calling it ends the chain right there,** and your return value is the answer. On `tool.call` that's how you stop a tool.\n\n**Returning nothing is a failure,** and a failure doesn't stop anything. The second half of this page is about why.\n\n## The two axioms\n\nThe cheat sheet pairs these, and between them they cover what `next` means on any event:\n\n> core has a side effect: no `next` = it did not happen, `next` twice = it happened twice\n>\n> core has no side effect\n\nOn `tool.call` the side effect is running the tool. That leads straight to the most misunderstood thing in this runtime, so here it is in Anthropic's words:\n\n> The return of 'deny' here is not somehow making the tool not get called. To make the tool not get called, don't call `next(e)`. When you hook onto `tool.call`, you are *responsible for calling the tool*.\n","previous_page":"anatomy","next_page":"engine","url":"https:\/\/changelogs.core-directive.com\/reference\/mods\/lifecycle","index_url":"https:\/\/changelogs.core-directive.com\/reference\/mods.json"}