Hooks¶
Hooks run configured automation at mevedel lifecycle and tool boundaries. Use them for policy checks, formatting, linting, context injection, and notifications. Command handlers receive JSON; native Elisp handlers receive a mevedel event plist. The architectural rationale explains the event boundary and ordering choices.
Hook execution flow¶
flowchart TD
A[Stable mevedel lifecycle event] --> B[Collect configured hook layers in precedence order]
B --> C[Match event and target]
C --> D[Run handlers in order]
D --> E{Decision returned?}
E -- No --> F[Continue]
E -- Block or deny --> G[Surface reason and stop supported operation]
E -- Context or rewrite --> H[Apply allowed mutation]
H --> F
F --> I[Record hook log]
G --> I
Event set¶
Supported events and their control effects:
| Event | Fires | Matcher | Control |
|---|---|---|---|
SessionStart |
root context epoch begins | source (startup, resume, clear, compact, rewind, restore, fork) |
add context only |
UserPromptSubmit |
before a root or retained-agent task input is sent | none | block, add context, rewrite prompt |
UserPromptExpansion |
before a user $skill expansion reaches the model |
none | block, add context, rewrite prompt |
PreToolUse |
after validation, before permission | tool name | deny, ask, stop the turn, add context, rewrite args |
PermissionRequest |
once before any permission card enters the shared queue | tool name | allow, deny, ask |
PermissionDenied |
once after a final tool denial | tool name | add feedback/context only |
PostToolUse |
after handler and result shaping | tool name | stop the turn, add context, replace result, mark feedback |
PostToolUseFailure |
after a handler reports or signals failure | tool name | stop the turn, add context, replace result |
PreCompact |
before manual/automatic compaction | trigger (manual, auto) |
block, add context |
PostCompact |
after compaction completes | trigger | notification/logging |
SubagentStart |
once before a retained-agent identity is published | agent role | block, add context |
SubagentStop |
after each retained-agent turn reaches terminal status | agent role | notification/logging |
Stop |
after a successful top-level assistant turn | none | notification/logging |
StopFailure |
after an errored or aborted top-level assistant turn | none | notification/logging |
SessionEnd |
once per live root session epoch: data buffer killed or Emacs exits | reason (kill-buffer, exit) |
notification only |
Config shape¶
Persistent hook configuration accepts Lisp data and JSON. Lisp is idiomatic for Emacs users and can name Elisp functions naturally; JSON is natural for shell-heavy hook configs and easier to share with other agent tools. When both files are present in the same layer, load and merge both additively in documented order.
Lisp shape:
((PreToolUse
((:matcher "Bash"
:hooks ((:type command
:command ".mevedel/hooks/block-rm.sh"
:timeout 10
:description "Block destructive rm")
(:type elisp
:function my-mevedel-bash-policy)))))
(PostToolUse
((:matcher "ApplyPatch"
:hooks ((:type command
:command ".mevedel/hooks/format-changed-file"
:timeout 30))))))
Configuration sources:
mevedel-hook-rules: user defcustom, Emacs-local.~/.agents/hooks.el,~/.agents/hooks.json,~/.mevedel/hooks.el, and~/.mevedel/hooks.json: user files, shareable across projects on one machine.- Activated plugin manifests may contribute hooks only after mevedel has
shown a concise consent summary for the plugin's executable hooks.
Mevedel reads the Codex default
./hooks/hooks.jsonwhen the manifest omitshooks, or the manifesthooksfield when present. That field must be a single path. <workspace>/.agents/hooks.el,<workspace>/.agents/hooks.json,<workspace>/.mevedel/hooks.el, and<workspace>/.mevedel/hooks.json: project hooks, trusted per workspace.- Skill frontmatter
hooks: scoped to a command invocation. Project skill declarations require exact workspace/path/content trust. Instruction preparation ignores this field. In fork commands, a localStopdeclaration is normalized toSubagentStop. - Agent definition
:hooks: scoped to invocations of that registered agent. A localStopdeclaration is normalized toSubagentStop. - Session/request/invocation hook lists: transient programmatic layers.
Hook handlers run from lower-precedence roots to higher-precedence roots
so later rewrite fields preserve the package-wide resource precedence.
Layers merge additively in this order: mevedel-hook-rules, user
.agents/hooks.el, user .agents/hooks.json, user
.mevedel/hooks.el, user .mevedel/hooks.json, enabled plugin hooks,
trusted project .agents/hooks.el, trusted project .agents/hooks.json,
trusted project .mevedel/hooks.el, trusted project
.mevedel/hooks.json, session, request, and agent invocation. Command-skill
hooks are folded into the owned request or agent invocation. Deny decisions
remain restrictive across all layers;
allow decisions do not override existing explicit permission denies.
Within a file layer, .el runs before .json when both exist. Ordering only
matters for hooks that rewrite tool input or results, so this order is kept
deterministic.
JSON shape:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".mevedel/hooks/block-rm.sh",
"timeout": 10,
"failClosed": true
}
]
}
]
}
}
Matching¶
matcher is event-specific. For tool events it matches the mevedel tool
name (Bash, Read, ApplyPatch, Agent, MCP names when wrapped).
For agent events it matches agent role. For compaction it matches
manual or auto.
Matcher rules:
- nil, empty string, or
"*"matches all. - strings containing only letters, digits,
_,-, and|are exact names or pipe-separated exact alternatives. - any other string is an Emacs regexp matched case-sensitively.
- a regexp that does not compile is dropped with a warning naming its event, so a typo cannot abort the lifecycle event the group was configured under.
Handler-level :if predicates are unsupported.
Handler types¶
command runs a shell command with JSON input on stdin and captures
stdout/stderr. Execution follows the configuration resource's origin:
- trusted project-file commands run on the session target from the workspace root through the target's Emacs file handler;
- user defcustom commands run locally from
user-emacs-directory, user-file commands run locally from the directory containing the user hook file; and - plugin commands discovered inside the project run on the session target from the plugin root; client-local plugin commands remain local there.
Commands executed on a remote session target inherit that target's shell
environment. Mevedel does not forward the client's process-environment;
project plugins receive only their deliberate compatibility variables in
addition to the target environment. Client-local user and plugin commands
continue to inherit the client environment.
Loaded handlers retain :source-file and :source-root metadata so dispatch
does not infer origin from the event cwd or copy executable code between
machines. Project-relative commands such as
.mevedel/hooks/block-rm.sh therefore resolve consistently regardless of the
session cwd. A command handler without trusted project, user, or plugin
provenance is refused before launch. Default timeout is 30 seconds with a
global cap, armed as soon as the child exists rather than after its stdin is
written. Remote commands acknowledge startup before mevedel writes JSON: this
prevents the preceding transport shell from reading ahead into input and
discarding it on exec. Waiting for that acknowledgment uses the same timeout
and cancellation ownership; the internal acknowledgment is not hook output, and
event payloads remain on stdin rather than in process arguments.
Whatever settles a command handler — exit, timeout, request
cancellation, or a failed stdin write — leaves no child running and writes
one log entry. If the child exits before the stdin write, its exit status
determines the decision. A cancellation logs status cancelled and closes the
event's telemetry span without running the remaining handlers or the
caller's continuation, because the request that owned them is being torn
down. Cancellation also stops command startup before preparation or launch;
resources returned after cancellation during yielding startup are released.
Stop and StopFailure handlers remain independent of request cancellation
and retain their own timeout. Command completion waits for idle transport before
advancing the handler chain when its sentinel interrupts remote I/O. The first terminal result is
retained, the child is released immediately, and request cancellation remains
effective while that continuation is queued. This applies to local predecessors
too, because a later handler may be remote. Remote command exit handling runs
after TRAMP drains stderr; local command exit handling drains its separate
stderr pipe up to the output cap or a 50 ms drain budget, so a surviving writer
cannot monopolize exit handling even with truncation disabled. Both preserve final diagnostics before logging
or building a block reason. Each
stdout/stderr stream is capped by mevedel-hooks-command-output-max-chars
before parsing decisions or writing log previews, so noisy hooks cannot
inject unbounded output through updated_result or block reasons.
Plugin command hooks receive compatibility environment variables:
PLUGIN_ROOT,CLAUDE_PLUGIN_ROOT, andMEVEDEL_PLUGIN_ROOTpoint at the plugin root.PLUGIN_DATA,CLAUDE_PLUGIN_DATA, andMEVEDEL_PLUGIN_DATApoint at<workspace>/.mevedel/state/plugin-data/<plugin-name>and are created before the command starts.
For a project plugin on a remote target, all six values use target-native paths. A plugin root naming another execution target is refused before the command process starts.
A client-local plugin in a remote session receives the three root variables but not the three data variables: its workspace data is authoritative on the target, and a local command cannot consume a TRAMP file name. Target identity and target-native workspace facts remain available in the JSON event input.
Codex plugin apps and mcpServers manifest fields are not loaded by
the hook subsystem. They are unsupported plugin components.
elisp calls a function with one event plist argument. Functions may
return nil or a decision plist. Elisp hooks are trusted in-process code
and should usually be configured by the user, packages, or skills rather
than accepted blindly from untrusted project files.
Native abnormal *-functions hooks and declarative elisp handlers enter
one ordered handler engine. Native functions are represented as Elisp handler
records ahead of the declarative layers while retaining Emacs buffer-local,
global-inheritance, and hook-depth ordering. The shared engine owns decision
normalization, one log record per handler, serial payload mutation, decision
merging, error policy, and event-specific terminal short-circuiting. Normal
zero-argument mevedel-session-start-hook and mevedel-session-end-hook
remain notification hooks outside this decision engine.
Only command and elisp handlers are supported.
Input and output¶
Every event plist includes these keys; values can be nil when no session or turn is available:
:hook-event-name:session-id:transcript-path:cwd:workspace-root:model:turn-id:origin(/rootor a canonical retained agent path)
Tool events add:
:tool-name:tool-use-id:tool-input:raw-resultfor post events, before persistence/truncation and render-data shaping:result/:tool-responsefor post events, containing the current result with UI-only render data removed and media represented for hooks. Provider output persistence/truncation happens afterward. Both names are provided;:tool-responseis the documented hook payload field, while:resultis kept as an Elisp convenience.:errorfor failure events
Prompt events add:
:prompt: the user prompt about to be sent:display-text: optional view-facing text used when the actual prompt is generated from another source, such as an inline skill invocation:skill-nameand:argumentsforUserPromptExpansionwhen the prompt came from a$skillinvocation
Compaction events add:
:trigger:"manual"or"auto":tokens-before:aggressive:instructionsforPreCompact:summaryand:tokens-afterforPostCompact
Sub-agent events add:
:agent-path:role:description:transcript-relative-path:promptforSubagentStart:statusand:terminal-reasonforSubagentStop
Top-level terminal events add:
:status, currentlycompleted,error, oraborted:terminal-reasonforStopFailure
Command handlers receive the same data encoded as JSON with snake_case
keys. Their cwd, workspace_root, and transcript_path values are
target-native paths. Remote file names nested in tool_input are target-native
too, while ordinary strings such as Bash command text are unchanged.
execution_target contains the session's structured target identity. This
lets a local user or plugin hook reason about a remote event without receiving
a client-specific TRAMP prefix or pretending to have a remote process cwd.
Elisp handlers receive the plist directly, with Emacs-qualified paths and
:hook-handler holding the normalized handler metadata for declarative
handlers.
Decision plist fields:
:continue nil: stop processing where supported. From a tool event it ends the turn at the current tool boundary (see below).:stop-reason: user-facing reason.:system-message: user-visible warning/status.:additional-context: developer/model context to inject into the next request or current tool feedback, depending on event. ForPreToolUse, delivery is in the eventual tool result, after the current attempt. Use deny/ask/input rewriting when the current action must change; a textual instruction cannot retroactively constrain execution.:permission-decision:allow,deny, oraskfor pre-tool and permission events.:permission-reason: model-facing reason for deny/ask feedback.:updated-input: replacement prompt text forUserPromptSubmitandUserPromptExpansion, or replacement tool args forPreToolUse. Prompt rewrites create a hook audit surface attached to the submitted user turn. The audit surface records the hook event, optional system-message/reason detail, and original versus submitted text; it does not need inline diff review UI. Tool argument rewrites create a hook audit surface attached to the affected tool attempt, recording original and updated args.:updated-result: replacement result for post-tool events.:suppress-output: unsupported; returning this field produces a hook error.
Command exit code handling:
- exit 0 with empty stdout: success, no decision.
- exit 0 with JSON stdout: parse as decision.
- exit 2: deny/block/continue according to event, using stderr as reason.
- other non-zero: hook failure. Log it and fail open unless the event is explicitly configured as fail-closed.
Handlers may set :fail-closed t. For those handlers, timeout,
unparseable required output, or non-zero failure blocks the triggering
operation with a hook-failure reason. The default remains fail-open so a
broken formatter or notification script does not strand normal work.
Pipeline integration¶
The tool pipeline shape is:
validate
-> PreToolUse hooks
-> normalize paths and prepare addressed resources
-> permission / PermissionRequest hooks
-> capture mutation coverage and optional snapshot
-> handler and render transform
-> PostToolUse / PostToolUseFailure hooks
-> provider projection: hook/repair context, oversized-output persistence,
Goal warnings, and render/media attachment
Running PreToolUse after validation means hooks see normalized args and
do not need to duplicate schema checks. Running it before permission lets
project policy deny early and lets PermissionRequest hooks participate
in every permission-card path. Generic, Bash, Eval, and sandbox-authority
requests all fire it once before shared queue admission. Hook allow or deny
settles the pending request without creating a card; displaying or
re-evaluating an admitted card does not rerun the hook.
PostToolUse runs after the handler and render transform, before provider
projection, and receives :raw-result, :tool-response, and :result.
The latter two contain the current feedback; subsequent provider projection
can add context or persist oversized output. :raw-result is available
for audit, formatting, redaction, or repair hooks that need the handler's
original output. Post-tool hooks cannot block already-completed tool side
effects; they may replace feedback with :updated-result or add context.
A :continue nil decision from PreToolUse, PostToolUse, or
PostToolUseFailure stops the turn, matching Claude Code's continue: false.
PreToolUse still denies its tool call. The turn then ends at the current
tool boundary: the batch's tool results are recorded and the turn settles
normally instead of sampling the model again. On a root turn an active Goal
is paused with the stop reason, because continuation would otherwise restart
the stopped work; an agent's stop ends only that agent turn. A fail-closed
handler's failure is such a decision too. When several handlers run, any
stop wins: a later continue: true does not cancel it.
Only a handler execution reaches a post-use hook: canonical success emits
PostToolUse, while explicit or signaled handler failure emits
PostToolUseFailure. Validation, permission, and aborted-interaction
failures emit neither.
PostToolUse and PostToolUseFailure :additional-context attach their
hook audit surface to the affected tool transcript result because that is
the feedback they modify. The hidden audit block is stripped at
gptel--parse-tool-results before any model-bound tool result is built.
Hook steps must read session/workspace/default-directory from the pipeline context, matching the existing rule for all post-handler steps. The hook owner supplies tool-context accumulation, audit construction, and dispatch in the captured buffer. Both the pipeline and permission orchestration use those operations; permission handling can load and run without loading the execution pipeline.
Lifecycle integration¶
UserPromptSubmit runs in the data buffer context after any deterministic
skill plan has fully prepared its bodies, placeholders, and hidden instruction
context, but before the view renders or forwards the prompt to gptel-send.
Queueing runs no prompt hook; a queued entry fires this event once when it
becomes its own turn. A blocking decision stops the send without
inserting a user turn. Its additional context remains pending on the root
session and is consumed once by the next accepted root input. For ordinary
prompts, :updated-input replaces the
prompt text. For a deterministic skill plan, the replacement is accepted
only when it retains the complete prepared prompt unchanged as a substring;
otherwise the rewrite and its audit record are ignored. This lets a hook add
prefix or suffix policy without deleting a prepared body, reminder, or
placeholder. Only an explicit blocking decision cancels the whole plan.
:additional-context is appended to the model-visible prompt inside a
<hook-context> block while staying out of the view-facing user message
body. The view shows a generic collapsed ◇ hook context added
disclosure that can be expanded to see the contributing event names and
injected text. Multiple hook context contributions consumed by the same
prompt share one combined disclosure, preserving contribution order in the
expanded details. Pending context is cleared at the transcript commit boundary,
before request startup, so a later dispatch error cannot duplicate context that
is already stored. An error before transcript insertion leaves it pending for
retry. If a prepared WaitAgent steering attempt loses its waiter race, mevedel
transfers the approved context into the queued prompt. Earlier FIFO entries
cannot consume it, draining does not rerun UserPromptSubmit, and editing or
clearing the queue restores the context for the next user submission.
Model-visible and persisted <hook-context> blocks contain ordered
<hook-event name="..."> entries. When known, their source, file, and
plugin attributes identify the contributing handler's configuration source.
Attribution comes from the host runner, not handler-returned claims. A decision
whose context was changed after the runner returned keeps only its event label,
so replacement text does not inherit stale attribution. Attribute values and
bodies are escaped; authored obligations and repeated contributions remain
intact rather than being summarized or silently deduplicated. These source
labels support conflict assessment; they introduce no new precedence rule.
Resume, rewind, and full rerender retain the recorded context:
<hook-context>
<hook-event name="SessionStart" source="plugin" plugin="project-workflow">
Project workflow context.
</hook-event>
<hook-event name="UserPromptSubmit" source="project-file" file="/project/.agents/hooks.el">
Project-specific prompt policy.
</hook-event>
</hook-context>
Retained-agent initial tasks and idle-agent follow-ups also fire this event once before their model request. Mailbox delivery, compaction, guardians, response items, and automatic continuations do not. If an agent task is blocked, its additional context remains pending on that retained identity and is consumed once by its next accepted task; it cannot enter the root or a sibling conversation. Internal root flows that construct their own requests, such as directive processing and plan execution, do not currently fire this event.
SessionStart :additional-context is pending context for the next request,
not an immediate transcript event. Its hook audit surface is attached to the
first user turn that consumes it, so the visible record appears where the
context affects model input. A fresh buffer uses source startup; restoring a
saved session uses resume; successful /clear uses clear; and successful
root compaction uses compact. Restoring an already-live buffer does not
reinitialize it. Clear and compaction begin context epochs inside the same
live session epoch and therefore do not emit SessionEnd.
SessionEnd closes a live session epoch exactly once. Killing the root data
buffer reports reason kill-buffer. Emacs exit kills no buffers, so an exit
hook reports every epoch still open with reason exit before session
persistence saves state and releases ownership. Exit starts the handlers of all
open sessions together and waits for them to settle, so it is bounded by the
slowest handler's own timeout; quitting stops the wait. Read-only inspection
buffers have no epoch and report nothing.
Context-changing audit surfaces represent an event once and list its contributing handlers in execution order. Each handler retains its own source, identity, reason, and context bodies where that audit surface exposes them. Parent sub-agent rows omit the injected bodies.
UserPromptExpansion runs after each unique user-invoked skill body is
prepared. Commands run first from left to right, followed by deduplicated
instructions in first-occurrence order. :updated-input replaces that skill's
prepared contribution; :additional-context is appended inside a
<hook-context> block. A blocking decision stops the complete plan before any
request or child dispatch. After all expansions settle, UserPromptSubmit
sees the complete inert prompt without receiving earlier context in its event
payload. Model-side Skill calls do not fire this event. Expansion context is
merged afterward with start and prompt-submit context, in lifecycle order, and
persisted in the accepted turn's durable disclosure.
PreCompact runs after the compaction range and prompt have been prepared
but before the compaction request is sent. A blocking decision stops the
compaction. :additional-context is supplied as labelled untrusted evidence
beneath the fixed context-summary system contract, so hooks can provide local
retention hints without changing summary purpose, authority, or structure.
Its hook audit surface belongs on the compaction event/summary, not a
user turn, because the summarizer request is the model call it changes.
For automatic compaction, a block is treated as compaction failure and the
pending user request is not sent. Each provider retry reruns PreCompact with
a fresh decision rather than reusing the previous attempt's hook result.
PostCompact runs after a successful summary has been applied. It receives
the summary and before/after token estimates. Decisions are currently logged
but not injected anywhere. A successful root compaction then runs
SessionStart(compact) before final view completion. Manual compaction leaves
that context pending for the next accepted input; automatic compaction adds it
to the already-approved pending request without rerunning UserPromptSubmit.
Retained-agent compaction emits PreCompact and PostCompact only.
SubagentStart runs once when a retained identity is created, before
UserPromptSubmit and before the identity is published. Follow-ups and
retained-agent compaction do not rerun it. A blocking decision stops the Agent
tool without leaving an addressable identity or partial conversation.
:additional-context is appended to the sub-agent prompt inside a
<hook-context> block. This hook is awaited before the atomic spawn commits;
after commit the Agent tool returns the retained canonical path immediately.
Its hook audit surface is split across parent and child:
the parent Agent tool row records that the spawned agent received hook
context, while the child transcript attaches the full hook context to the
child's initial prompt.
SubagentStop runs once after every invocation reaches completed, error,
or aborted and after transcript status/sidecar updates have been written.
The retained identity remains addressable for later follow-ups.
Decisions are currently logged but do not change terminal status or parent
feedback.
Stop runs after a successful top-level assistant turn, before the
request-scoped hook layers are cleared. Stop and StopFailure retain the
ending request's hook rules and event context, but their handlers are not
registered as request cancellers: turn teardown must not kill them before
they settle. Each command handler remains bounded by its own timeout.
This includes awaited fork user skill
completions, which finalize the parent turn without a
normal gptel DONE transition. StopFailure runs for top-level error and
abort terminals and includes :terminal-reason when available. Both
events are observational: blocking decisions are logged but do not change
terminal state.
Trust and permissions¶
Shell hooks are arbitrary code in their resource environment. Project-file commands run on the target and must not run unless the workspace hook config is trusted. By default:
- user defcustom and user
.agents/hooks.*/.mevedel/hooks.*files are trusted. - project
.agents/hooks.*,.mevedel/hooks.*, andSKILL.mdhook declarations are ignored until the user trusts them for that workspace. - trust state lives under user state, keyed by workspace id and project hook file hashes.
- changed project hook files or skill manifests require re-trust.
- resolution reads each project config once, checks that snapshot's hash, and parses the same bytes; replacing the pathname after the check cannot substitute executable rules.
- trusting a project refreshes that workspace's trust entries to the current hook files and skill manifests only, so removed sources are no longer trusted.
Permission decisions are audit-surfaced by outcome. deny and forced
ask decisions are visible on the affected tool attempt because they
change control flow. allow decisions remain in hook logs unless they
suppress a permission prompt that would otherwise have been shown.
Project Lisp configuration is read as data with reader evaluation disabled;
loading it does not evaluate arbitrary forms. A trusted :function declaration
can invoke an existing Elisp function, which then runs with full in-process
authority. Trust the handler implementation as well as its configuration.
Hooks may tighten policy. They should not silently weaken explicit
permission denies. A PreToolUse or PermissionRequest allow can skip a
prompt only when the normal permission resolver would not return an
explicit deny. An ask still emits PermissionRequest once before a
PreToolUse allow suppresses queue admission. Every final policy, user,
PermissionRequest, or
PreToolUse denial emits one PermissionDenied payload carrying the
original :permission-provenance.
Emacs API¶
Notification hooks:
mevedel-session-start-hookmevedel-session-end-hook, withmevedel-session-end-reasonbound to theSessionEndreason while it runs
Control/argument hooks:
mevedel-user-prompt-submit-functionsmevedel-user-prompt-expansion-functionsmevedel-pre-tool-use-functionsmevedel-permission-request-functionsmevedel-permission-denied-functionsmevedel-post-tool-use-functionsmevedel-pre-compact-functionsmevedel-post-compact-functionsmevedel-subagent-start-functionsmevedel-subagent-stop-functionsmevedel-stop-functionsmevedel-stop-failure-functions
These support normal add-hook usage, including buffer-local hooks
with LOCAL non-nil. Programmatic hooks use the same decision plist
as declarative elisp handlers.
Debugging and UI¶
The hook runner keeps a per-session in-memory hook log with event,
handler, status, elapsed time for command hooks, stdout/stderr previews,
parsed decision, and failure details. SessionStart entries also record
:event-source as "startup", "resume", "clear", or "compact",
separately from the handler's configuration source.
Raw command-hook stdout/stderr never appears in the view by default.
Only structured decision fields are eligible for user-facing hook audit
surfaces. Hook authors who want user-visible text must return fields such
as :system-message, :stop-reason, :permission-reason,
:additional-context, or :updated-input.
:system-message by itself is a transient user notification: it is shown
through message and recorded in the hook log, but it does not create a
transcript/view audit item. When the same decision also changes
model-visible context, submitted content, control flow, or permissions,
the hook audit surface includes the system message as supporting detail.
When the session has been materialized on disk, the same entries are also
appended to <session>/hook-log.el as one sanitized plist per line. Local
sessions append each entry in UTF-8, without rereading the log or checking an
already-existing parent directory; remote process callbacks enqueue
and the next successful session settlement publishes the append atomically.
Bounded entries created before the session has a save path are backfilled when
it first materializes. Non-readable runtime values such as closures are
converted to printable strings before writing. Hook and permission logs share
the diagnostic value/line codec in mevedel-utilities.el; their append, queue,
and failure policies remain owned by each log. The in-memory log remains
capped by mevedel-hooks-log-limit; the persistent file is append-only for the
session. A failed append warns, remains queued, and retries after the next
successful session save.
Raw hook stdout/stderr should stay out of the model transcript by default.
Only explicit structured fields such as :additional-context,
:permission-reason, :updated-result, or :tool-response may enter
model-visible content. Random script output belongs in the hook log, with
important failures surfaced to the user in the view or messages.
Slow hook runs are surfaced after mevedel-hooks-slow-threshold seconds.
If the view already has an active spinner, its status changes to show the
running hook event; otherwise the user sees a message. The temporary
status has a per-run ownership token. It replaces only the progress state
captured when the hook was dispatched, restores that state on completion,
and cannot overwrite or restore across a newer request or agent status.
Events with no matching handlers do not create progress activity. Blocking
decisions are always surfaced with the event name and hook-provided reason.
For tool calls blocked by PreToolUse or PermissionRequest, the compact
tool line includes a second line such as blocked by PreToolUse: reason.
Useful commands:
mevedel-hooks-list: show effective hooks for the current session.mevedel-hooks-trust-project: trust the current project's hook files and skill manifests.mevedel-hooks-run-dry: show which native and declarative hooks would run for an event/matcher target without executing Elisp or shell handlers.mevedel-hooks-reload: forget the memoized hook configuration so the next event re-reads every config file. The user, plugin, and project layers are remembered per workspace for the process — re-resolving them costs several target round trips per event on a remote workspace — so an edit to a hook file needs this command (or trusting the project, or toggling a plugin, each of which invalidates) to take effect.
Quiet successful hooks do not clutter the normal workflow.