Docs/Reference

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.

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.