Skip to main content
A hook lets a plugin act when something happens in a session: a tool is about to run, a tool has finished, a session has started. A hook is declared by a plugin, in that plugin’s manifest, and it runs inside the plugin runtime. There is no separate hooks file to write by hand. This page covers where a hook is declared, which events actually fire today, the order hooks run in, what a hook is allowed to change, and the one thing a hook can never do: approve something on your behalf.

Where a hook is declared

Hooks live in the plugin manifest, as one [[hooks]] block per hook:
A hook block has exactly four keys, and the manifest refuses any key it does not recognise rather than ignoring it: Because a hook belongs to a plugin, installing a plugin is what installs its hooks. Creating, building, validating and installing a plugin is covered on CLI plugins.

What fires today

This is the part to read before you design anything around hooks. The manifest accepts a broad set of event names, but only some of them are dispatched by the shipped build. Hooks run in two situations:
1

When a session loads your plugins

Starting the CLI loads your plugin directories into the plugin runtime and fires session_start. A plugin that refuses here stops the runtime from coming up.
2

When you invoke a plugin directly

cortex plugin run dispatches the full chain around the call: session_start, then tool_execute_before, then the tool or command itself, then tool_execute_after, then session_end. A hook that denies at session_start or tool_execute_before stops the invocation and the reason it gave is what you see.
Every other event name is accepted by the manifest but is not dispatched yet. A hook declared on one of them is valid, installs cleanly, and never runs. In particular, a hook does not fire around the tool calls the model makes during an ordinary interactive turn, so a hook cannot be used to police an interactive session today. Use Permission policy for that.

The events

Names are written in the manifest in lower case with underscores. Listings elsewhere in the product print the same events in a dotted form, so tool_execute_before appears as tool.execute.before. The manifest only accepts the underscore form. The manifest also accepts a set of interface-level events for extending the terminal interface itself. None of them are dispatched yet either, and they are not useful to target today.

Order and matching

When several hooks listen for the same event, the order is deterministic: by priority first, lowest number first, then by plugin identity to break a tie. Two plugins that both ask for priority 50 will always run in the same order relative to each other, run after run. pattern decides whether a hook is offered the event at all: One * at the start or the end is a prefix or suffix match. A pattern with no * is an exact match, not a substring search.

What a hook may not do

A hook that fires before an action can change the payload it was given, and it can refuse the action outright with a reason. Both of those are bounded:
  • It cannot change which tool is being called, and it cannot remove the arguments. Trying to is refused with A hook cannot change tool identity or remove arguments.
  • It cannot raise the role of a message. Trying to is refused with Hooks cannot elevate message roles.
  • tool_execute_after, session_end and error_handle are observers. They see the outcome and can report on it, but they do not replace a result or reverse what already happened.
A hook can say no. A hook can never say yes. Every permission question starts at “ask”. A hook is allowed to answer “deny”, and the first hook that answers anything other than “ask” settles it. If a hook answers “allow”, the dispatch does not quietly accept it: the call fails with Plugins cannot grant execution privileges. A third-party plugin attempting it is refused with Third-party plugins cannot auto-grant permissions (security restriction), and the engine checks the same thing again independently. So a hook cannot stand in for an approval, and it cannot remove one. If an action would have asked you, it still asks you. Wiring a hook in the hope of skipping a prompt does not work, by design and in two places.

When a plugin is not active

Hooks are skipped for a plugin that is not loaded or has been disabled, quietly and without an error, which is what makes cortex plugin disable a complete off switch for that plugin’s hooks. A plugin that is in any other non-active state reports Plugin is not active instead of being skipped.

The hooks file and the /hooks command

Two things a reader often goes looking for are not available yet, and it is better to say so than to let you spend an afternoon on either. There is a configuration shape for standalone hooks, keyed by file pattern with a command, an environment and a timeout, and it is not read by anything in the shipped build. Writing that file has no effect. Hooks reach a session through a plugin manifest, as above. The /hooks command in the interactive interface is not wired either. Running it answers Unsupported command in this session: hooks:list. No operation was performed. Use cortex plugin list to see what is installed, and the plugin’s own manifest to see which hooks it declares.
  • CLI plugins - installing, trusting and building the plugin a hook belongs to.
  • Permission policy - the mechanism that does gate an interactive session.
  • Tools - the tools a tool_execute_before hook sees the name of.
  • Slash commands - the commands that are wired in the interactive interface.
  • CLI troubleshooting - a declared hook that never runs.