Reference
Common hook runtime and harness adapters
The enforcement states live in enforcement control; hook execution and harness translation are described here. An OMP write to xd://lsp is a tool-device request, not repository prose. Documented read-only actions—status, capabilities, diagnostics, definition, references, hover, symbols, type definition and implementation—use lookup admission with the existing remote-access and audit protections. The original outer request remains in evidence; pre-event, post-event and evidence routing use the same normalized identity. Malformed JSON or a missing string action refuses with its protocol cause. Rename, rename-file, applying code actions, unknown actions and raw requests do not receive this read-only classification.
What owns the execution loop
Tama owns registry loading, hook execution, enforcement selection and reload.
Harness adapters translate their incoming events and outgoing decisions; they
do not implement another copy of the hook rules. The native engine is a library
inside the one tama program, which runs it as
tama run-hook.
The one-event interface remains
tama run-hook <event> <agent-id>, with the payload on standard
input. Claude receives the canonical decision and exit code 2 on
a policy block. Codex receives its existing minimal block response and exit
code 0; a passing Codex decision produces no stdout. Those are
adapter differences, not different policy evaluations.
The installed run-hook.mjs entrypoint is a process transport to
hooks-runtime/current/bin/tama run-hook, not a second evaluator.
It preserves stdin, stdout, stderr, exit status and signals. Failure to start
the native engine, or a registry startup failure, exits 1 with
TAMA_HOOK_INFRASTRUCTURE; it does not fall back to a JavaScript
policy engine or report a passing policy decision.
The agent id is the argument after the event, else TAMA_AGENT_ID.
With neither the run is refused with TAMA_HOOK_INFRASTRUCTURE: tama run-hook
needs an agent id: pass it after the event or set TAMA_AGENT_ID and exit
1, since a run for no agent publishes no session record.
TAMA_AGENT_ID, TAMA_PROVIDER, TAMA_ONLY_HOOK_ID,
TAMA_DISABLED_HOOKS and TAMA_ENABLED_HOOKS holding a value
that is not text are refused the same way with <name> cannot be read:
<error>, instead of read as unset.
#Which tool calls a gate reads as a shell or a write
Harnesses spell the same tool differently: Claude sends Bash,
OMP sends bash, Codex sends exec_command, and the
function bridge prefixes functions.. Every gate that reads a
shell command or a written file asks Tama's one tool declaration, after
lower-casing the payload's tool_name. The shell spellings are
bash, functions.bash, exec_command,
functions.exec_command, shell, exec,
run_command and run_package_script; the write
spellings are the whole-file writes, anchored and multi edits, the notebook
tools and the patch tools in each of their names. A gate never keeps a
shorter list of its own: a list that held Bash alone let every
OMP shell call through unread. To see a gate read an OMP call, pipe it the
payload directly:
printf '%s' '{"tool_name":"bash","tool_input":{"command":"git commit --no-verify -m x"}}' \
| tama hook block-git-hook-bypass
The refusal on stdout names the bypass; a gate that printed nothing for this payload is not reading OMP's shell spelling.
#Persistent runtime protocol
tama run-hook --serve omp
This starts the common engine, not an OMP agent. It first emits a JSON
ready frame containing its state, then accepts one JSON object per
line on stdin and returns one JSON object per line on stdout. Frames must end
with LF. Closing stdin ends the process.
{"id":"inspect","op":"status"}
{"id":"reload","op":"reload"}
{"id":"event","op":"dispatch","event":"pre_tool_use:read","payload":{"session_id":"example-session","tool_name":"read","tool_input":{"path":"example.txt"}}}
Responses carry the request id, ok, a
result or error, and the observed state.
Status requests include running-hook inspection in result and
state.runningHooks. Its dispatched list names every
hook this runtime is running now — session, event, hook id, process group
and runningForMs — from the runtime's own dispatch registry, so a
hook in flight is named even where no other process may be inspected; the
records left by a runtime that is gone follow under running, and
recordsUnread says why they could not be read. dispatchStages
says where each dispatch spends its life, from the runtime's own clock:
running lists every dispatch in flight with the stage it is in
and inStageForMs, and lastByEvent holds the last finished
dispatch of every event with each stage's atMs and tookMs and
the slowestStage. The stages are received (taken off the
pipe), runtimeLocked (the runtime lock was free), registryRefreshed
(the hook registry re-read), threadStarted, sessionStateRead
(the session's overrides, capability and enforcement selection read),
hooksEnded (a tool call's or turn end's hooks all ended; each hook's own
ms and startMs are in lastDecision.results) and
answered. A tool call refused at the harness's limit while every hook answered
in milliseconds names here the stage that took the time; time before
received was spent in the adapter or the pipe, not in the runtime. A successful reload returns
{"reloaded":true,"changed":true}, or changed:false
when the validated catalog already matches. The response is produced after
the engine has adopted the candidate, never after inserting a prompt.
Answers arrive as requests finish, not in the order they were sent, and a
client matches each to its request by id. Each
dispatch runs on its own thread against the generation loaded
when it arrived, so a hook that never ends holds only its own request;
a status read runs on its own thread outside the runtime lock, because
inspecting running hooks can ask the operating system about processes, and on the
reading thread under the lock it held every dispatch that arrived meanwhile unread;
reload is answered at once. The OMP
adapter keeps every request in flight side by side and orders only reloads and
shutdown.
Within one dispatch the hooks of a tool event run in registry order, one
after another, and the first refusal ends the dispatch. The selected
block-supervisor-stop policy runs in-process first, reading only
Oko's local role and executor-work state. Its refusal returns without starting
any other stop hook (see completion work).
When it passes, the remaining stop hooks
run side by side and the engine waits for all of their reasons.
Checking that remaining review then takes as long as its
slowest hook, not the sum of them all. Run one after another, their times
added up past the handler budget OMP gives a session_stop
handler; OMP then settled the turn as if no hook had refused, and the agent
ended its answer with work still open. The refusal text and the
results records still follow registry order, and every record
carries the hook's own ms, so a slow stop hook is named by its
record. tests/completion/side_by_side.rs in the Tama repository
runs the built engine's stop on the installed registry and its
real hooks, and fails when the dispatch lasted as long as its hooks' times
added together.
Each hook runs in its own process group. A session ends its turn and
submits a prompt one at a time: the harness sends the next stop
or user_prompt_submit only after it stopped waiting for the one
before. So when one of those hooks starts while the same hook of the same
session (session_id in the payload) still runs for the previous
event, the engine ends the earlier run's process group — the hook and what it
started, such as a model request that is never answered — and writes
tama: ended hook <id> (process group <pid>) still running for
the previous <event> of session <id> on stderr; the ended run
is reported as cancellation, not as a hook malfunction or a passing verdict.
No clock is involved.
A newer tool call does not end an older one, because a session runs tool
calls side by side. The end of the turn does: the harness sends
stop only after every tool call of the turn is over, and
user_prompt_submit only once the turn before it ended. A
pre_tool_use or post_tool_use hook of that session
still running at that moment guards a call the harness has already refused at
its own handler limit, so the engine ends its process group as well and writes
tama: ended hook <id> (process group <pid>) still running for
<tool event> of session <id>: the session's <event> arrived, so
that tool call is over and nobody waits for its answer on stderr. Left
running, every such hook would keep one unanswered model request open, and
each further tool call would add another to a gateway that already does not
answer. That tool call's dispatch starts none of its remaining hooks and
answers ok:false with the turn of session <id> ended
while <event> was still being checked, so that tool call is over and hook
<id> and the ones after it were not started. A serving engine
whose input closes ends every hook run it still holds the same way, with
the core's caller closed its input, so nobody waits for its
answer. A payload without a session id is never superseded by another session event.
tama hooks running lists every hook still running.
Within a turn, a call's own result ends its check. OMP stops waiting for an extension's
tool_call handler at its own limit and does not tell the extension, so the pre-tool hooks of that
call would keep running until the turn ends. When the post_tool_use dispatch of a call (same
session_id, tool_name and tool_input) arrives while a
pre_tool_use dispatch of that call is still in flight, the engine cancels that dispatch and its hook
process groups and writes tama: ended pre-tool dispatch <id> of <tool> in session <id>: the
result of the <tool> call already arrived, so the harness stopped waiting for this check on stderr. Two identical calls
of one session in flight at once are one call to this reading. The journey
cargo test --manifest-path rust/Cargo.toml -p tama-cli --test abandoned_calls_answered sends a
pre-tool dispatch whose hook never ends and then the call's result, and requires the pre-tool dispatch answered and
named on stderr.
The harness's refusal at its own limit names no hook, so the engine names it. Whenever the harness's own
next signal proves it stopped waiting for a dispatch — the caller's cancel of the dispatch, the
call's result arriving while its pre-tool check still runs, a newer turn event superseding the previous one,
or the session's turn ending under a tool hook — the engine writes the hooks that dispatch still ran as
still running in Tama: <hook> (<event>, <ms> ms), one clause per hook, on stderr
with the signal that proved it. The last abandonment of every session is kept in the engine's memory, which a
status request reports as state.runningHooks.abandoned (session, reason,
abandonedAtMs, corePid, hooks with hook, event,
processGroup and ranForMs, and the sentence as text), and in the
session-control directory as abandoned.<session>.json, which tama hooks running and
oko sessions agents read after the engine is gone. No clock of the engine's own decides an
abandonment. A session id that cannot name a file is kept only in memory, and the engine says so on stderr.
The journey cargo test --manifest-path rust/Cargo.toml -p tama-cli --test hooks_running_abandoned
holds an edit on a hook that reads a pipe nobody writes, cancels the dispatch in one case and delivers the
edit's result in the other, and requires the status, the record on disk and tama hooks running to
name the hook, its event, how long it ran and the signal; an unreadable record is refused by name. The journey
hooks_running_held requires the status to name the hook a dispatch stands on while it runs.
#Cancellation, judge requests and hook supervision
How a dispatch is cancelled, how a judge request shares its adapter and waits only while Brama serves, and how each hook's processes are owned and ended: cancellation, judge requests and hook supervision.
A client can send knownCatalogChecksum. When it differs from
the loaded checksum, the response also includes the loaded catalog,
including on a rejected replacement. Matching clients receive no duplicate
catalog. Adapters can therefore update their display and permission bindings
from the engine's observed snapshot instead of racing a second file read.
Every dispatch also checks the registry. A valid changed catalog is adopted automatically before its hooks execute, without restarting the engine or changing the event's session identity. The process starts each hook command afresh; script and executable code is not cached between invocations.
The persistent engine leaves the harness-owned session record unchanged. It reads current session permissions for each event and supplies a capability to hook commands only when its session control key, release and catalog checksum match the loaded policy. The OMP adapter rebinds the grant its session holds whenever it loads another release, so an install or reload keeps the operator's consent; a capability written for another generation outside that reload does not authorize newly loaded hooks. One-event adapters still publish their own session records.
#Rejected replacements and observed state
A registry read, JSON parsing, shape or declared-checksum failure leaves the
preceding loaded generation intact. A dispatch encountering that failure
returns ok:false and executes no hook; it does not quietly run
the preceding policy instead. After a valid replacement is published, a later
dispatch can adopt it without restarting the process.
Diagnostics name the failed registry path and operation, for example
parsing hook registry <path>: EOF while parsing a value at line 2 column 0
or hook registry <path> checksum mismatch: expected <declared>, actual <computed>.
State includes processId, generation,
loadedReleaseId, catalogChecksum,
registeredHookCount, reloadCount,
lastReloadAtMs and lastReloadError.
processId identifies the common engine process, not its caller's
harness process. A registry without release metadata reports a null release
identity, not a fabricated release. A reload replaces the catalog the engine
dispatches, never the engine's own code, so a status answer also
names coreReleaseId, the release of the first registry the process
read (the code it runs: status, the in-flight hook records' inspection, the
abandoned-dispatch register), and coreReplacedByRelease, true when
a reload (or a dispatch that refreshed the registry) has since loaded another
release's catalog and the engine's own code stays the older one. The adapter
reads that in every answer: the first request that finds such an engine owing
no answer closes its input, waits for it to exit and starts the installed
release's engine, so a session never keeps running engine code its release
replaced; requests still owed an answer finish on the older engine first. The
real journey tests/core-replacement/journey.mjs reloads a registry
that names another release and requires the next status to come from a new
engine process on that release, then from the same one.
#OMP integration status
The omp-shared-hooks.js adapter forwards ordinary hook events and
reload operations to a persistent tama run-hook --serve omp process.
The reload tool and slash command return after the native operation completes;
they do not insert a prompt or ask OMP to restart. Runtime status includes the
observed native core.processId, separate from the OMP session pid.
Anchored edits accept the patch string in patch, input, raw_input or content. Each file header becomes its own hook target; the session directory is never a substitute for a missing target. A multi-file edit must be assessed for every target before the harness applies it.
OMP imports an extension module once per process and has no way to import it
again, so omp-shared-hooks.js is only a shell: it imports the adapter
itself, from shared-hooks/omp/adapter.js, under that file's SHA-256
digest, and forwards every event, tool and command the adapter registers.
tama_reload_hook_runtime, as a tool or as the slash command
/tama_reload_hook_runtime, first compares the installed adapter's digest
with the loaded one. When they differ it imports the installed adapter in place:
the new module is imported first, then the old adapter's
session_shutdown handlers run, the new adapter registers itself and its
session_start handlers run with the reload's context. The reload then
reports Loaded the installed OMP adapter <digest> in place of
<digest> before the engine's own answer, so a gate added in a new Tama
release (such as block-text-outside-tool-calls on
before_provider_request) runs in an open session without restarting OMP.
An adapter that fails to import or register is refused with
Tama could not reload its OMP adapter: <reason>, and the adapter
already loaded keeps running.
The shell also exposes the same adoption to the adapter's validated
reload-adapter session-control operation. Use
tama sessions reload --session ID --json or
Session → Reload adapter in Tama Desktop. The receipt names
the same process and session and its actual runtime.adapterDigest;
adapterReloadSupported distinguishes an entry with this control
path from an older already-loaded shell. A source update or an engine reload
alone is not a JavaScript adapter update. Concurrent adoption requests share
one operation. The digest is published by the replacement's activation; a
failed activation restores the prior digest with the prior adapter.
The adapter reads its loaded digest from the immutable module URL assigned by the importing shell, not from the current file on disk. An older shell can therefore publish the adopted adapter's identity after its in-terminal reload without acquiring remote reload support. A module imported without a digest reports no identity rather than claiming the installed bytes are loaded.
Two limits remain. The adapter's sibling modules (hook-exec.mjs,
session-control.mjs and the rest) are imported by the adapter once per
process, so a change to one of them needs OMP to start again; the reload's
answer says so. And a session that started before this shell existed holds the
old single-file adapter, which cannot reload itself: it needs OMP to start again
once (/restart keeps the session).
Every model request is decided at once, never by waiting on the
before_provider_request hooks: OMP waits for a handler only up to its
own budget and then sends the request unchanged, so a slow read of Oko's ledger
used to let prose through exactly when Oko was under load. The adapter answers
each request from the newest answer block-text-outside-tool-calls gave
in the session and starts one fresh dispatch in the background (never two at
once) whose answer decides the next request. For an OpenAI function-tool payload,
a refusal sets tool_choice: "required". Before the first answer,
that supported request shape is also forced. A request decided while work just
closed can therefore require a tool once more.
Anthropic Messages requests are not forced: models that reject
tool_choice types any and tool would
otherwise fail before doing any work. Unrecognized tool shapes also remain
unchanged, with the limitation reported on stderr. The completion gate still
checks these turns, but it does not prevent streamed prose from appearing.
Provider acceptance, prevention of prose, and completion enforcement are
separate outcomes; a successful model request does not prove all three.
The same handler gives every model request the contracts Jeden installed most
recently. jeden contracts install --omp writes them between the lines
<!-- jeden contracts: start --> and <!-- jeden contracts: end -->
in ~/.omp/agent/APPEND_SYSTEM.md, and OMP appends that file to a session's
system prompt once, when the session starts, so a contract changed later reached no
running session. The adapter (shared-hooks/omp/installed-contract.mjs)
reads the file's marked block on each request and puts it in place of the marked
block the request carries, wherever that block sits in the payload; a request with
no marked block, or a file without one, goes unchanged. A file that exists and cannot
be read sends the request under the session's own copy and prints
Tama cannot read the installed contracts <path>: <reason>; this model
request keeps the contracts its session started with on stderr.
jeden contracts status --omp names the sessions that started before the
file was last written.