Hook configuration and discovery: enabling and selecting internal hooks

Explains how the Gateway locates internal hooks across sources and how to enable and select which ones run. Useful for operators configuring predictable hook behavior.

Read this when

  • You are enabling internal hooks or narrowing which ones load
  • You need the master switch and selection rules for directory hooks
  • You need discovery precedence across bundled, plugin, managed, extra, and workspace sources

How the Gateway locates internal hooks across sources, plus how to turn them on and pick which ones run. This page belongs to the Hooks guide.

Configuration

To get a selection you can predict, turn on hooks by name instead of switching on wide discovery:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "command-logger": { "enabled": true },
        "session-memory": { "enabled": false }
      }
    }
  }
}

The master switch and the rules that decide which directory-loaded hooks get selected are:

ConfigurationSelection
hooks.internal.enabled: falseInternal hooks are off.
No master flag and no enabled entries, extra directories, or tracked installsGateway skips directory-hook loading.
Named entries, with master flag omitted or trueEnabled names form an allowlist; enabled: true on the master does not broaden it. An entry without enabled: false contributes its name.
Master flag true with no named entries or named installsOpen-ended discovery of eligible hooks.
Tracked hook packs declaring hook namesThose names join the selection; an explicit per-hook enabled: false still disables a non-plugin hook.
Nonempty load.extraDirs, or a tracked install without a hook-name listOpen-ended discovery, not a selection restricted to that directory or pack.

Workspace hooks never run without entries.<hookKey>.enabled: true, even when discovery is open-ended. For other file hooks, you can select an entry either by its name or by hookKey, though settings are read under hookKey. The CLI works out the name and writes the right key for you. Be aware that adding the first named entry may narrow a selection that was previously broad, so check the hooks already present before you change anything.

Per-hook entries accept arbitrary fields defined by the handler. The core types enabled as a boolean and env as a string-to-string map; custom handler options go unvalidated. For example:

{
  "hooks": {
    "internal": {
      "entries": {
        "my-hook": {
          "enabled": true,
          "env": { "MY_HOOK_LABEL": "example" }
        }
      }
    }
  }
}

Per-hook env satisfies eligibility checks but does not mutate process.env. When an event carries config, a handler can read it from event.context.cfg?.hooks?.internal?.entries?.["my-hook"]?.env. No cfg field is promised on other events. Do not log entire config objects or put secrets in examples.

Warning

hooks.internal.handlers is retired and fails normal config validation. Before running openclaw doctor --fix, migrate each registered module into a managed or workspace hook directory with HOOK.md and a handler. Doctor removes the old registrations; it does not create executable files. For a legacy-only config with hooks.internal.enabled: true, it also removes that flag to avoid broad discovery. Named entries, nonempty extra directories, and explicit enabled: false are preserved.

Hook discovery

Discovery across directories merges hooks by name, following these rules:

SourceLocation and collision behavior
BundledShipped with OpenClaw.
PluginHook directories declared by active plugins; can replace bundled names.
Managed<stateDir>/hooks/, normally ~/.openclaw/hooks/; can replace bundled and plugin names.
Extra directorieshooks.internal.load.extraDirs; same source policy as managed hooks. Later extra directories win over earlier ones; the managed directory wins over extra directories.
Workspace<workspace>/hooks/; can add names but cannot replace bundled, plugin, or managed names. Explicit opt-in required.

The bundled, managed, workspace, and plugin hook locations are collection directories: discovery inspects their immediate children for hooks or packages whose package.json declares openclaw.hooks.

Any explicit hooks.internal.load.extraDirs path may instead be a pack root, a single-hook root, or a collection directory. A pack root loads only the hook paths it declares, nested paths such as ./hooks/my-hook included. Every path has to point straight at a hook; discovery will not recurse into another pack or collection. A recognized pack with no valid hooks stays empty rather than scanning unlisted children. A single-hook root loads its own HOOK.md and handler. The immediate-child scan applies only to an ordinary collection root.

For instance, to select /opt/openclaw-hook-library/my-hook/HOOK.md directly, add that hook's directory:

{
  "hooks": {
    "internal": {
      "load": {
        "extraDirs": ["/opt/openclaw-hook-library/my-hook"]
      }
    }
  }
}

To scan the library's immediate children instead, add /opt/openclaw-hook-library. Add only directories you trust: any extra path opens hook-name selection across discovery sources beyond named entries, even when that path selects a single hook or pack. Handler files must stay within their hook directory; package and plugin hook paths must stay within their package root. Symlinks escaping those boundaries are rejected. Hook config and selected-workspace changes reload discovery in hybrid mode, including config written by a new hook-pack install or link. Hook files and metadata are not watched; restart after editing them or updating existing hook code, then verify the handler's actual side effect.

Hook packs

A hook pack is a package whose package.json declares hook directories in openclaw.hooks. Install a reviewed package or local directory through the unified installer:

openclaw plugins install <path-or-spec>

Installation and update flags, npm restrictions, linked-root behavior and trust, and the deprecated hooks install / hooks update aliases are documented in Install and update hook packs.

861 words · updated Oct 26, 2026