Reference
Hook releases
How does an approved hook tree become the runtime a session loads, and how does Tama know nobody changed it in between? Through one identity: a hook release is a directory whose name and manifest are the SHA-256 digest of its own contents, checked when it is sealed, checked before it is installed, and checked again after it lands. Hook policy evolves separately from the desktop app, so the two have separate identities — the product version policy is in releases.
#The release tree
Scripts/build-app.sh stages the release into
Tama.app/Contents/Resources/hooks-release from the hook source tree
selected by TAMA_HOOK_ROOT:
| Member | Content |
|---|---|
release.json | The seal manifest (below) |
shared-hooks/ | The registry, the hook scripts and what the harnesses load: the OMP adapter (omp/), session control (session-control.mjs) and the Node.js preflight (node-runtime-preflight.cjs) |
claude-hooks/, codex-hooks/ | Provider-specific hook entrypoints |
repo-githooks/<project>/ | Approved per-repository Git hooks |
external-hooks/ + external-sources.json | Approved hook sources that live outside the managed roots, packaged in with sourcePrefix → releasePath mappings (schema ai.wisent.tama.external-hook-sources.v1) |
bin/tama | The one sealed, signed Rust program: the backend, every native hook (tama hook <name>), the common hook engine (tama run-hook) and the MCP server (tama mcp) |
package.json | The hook package version |
Generator files (generate-configs.mjs, providers.json,
run-one-session-hook.js) and Python bytecode are pruned before sealing.
An installed tama reads them, like the registry, from the checkout
tama hooks release recorded in ~/.tama/installed-from:
tama hook run-one-session-hook resolves TAMA_ROOT first, then
that record, and only then the directory the binary was compiled in, which a
release build deletes.
#Sealing
tama hooks seal computes the tree digest: every file except
release.json and bin/ (signed after the seal; its code signature vouches for it), in sorted path order, hashing each file's relative path, its
permission bits, and its bytes, each length-framed. A chmod therefore
drifts a release exactly as a rewrite does. The digest becomes releaseId
in release.json, schema ai.wisent.tama.hook-release.v1, together with
packageVersion, catalogVersion, catalogUpdatedAt, sourceDirty,
sourceRevision (the hook source Git commit), and sealedAt. The build
manifest tama-build.json embeds the same object, so the app can display
its bundled hook identity without reopening the release.
The declared --source-root must be the actual Git checkout root.
Staging reads its HEAD and working-tree state, and records
sourceIdentity in native-hook-binaries.json:
revision, dirty, and a fingerprint of tracked
and non-ignored source inputs under package.json, the four hook
directories, and rust/. File paths, contents, modes and symlink targets
contribute to that fingerprint; build outputs are not source inputs.
The packager checks that staged source files match the checkout and that
source identity has not changed during compilation. Sealing checks the captured
identity again before publishing release.json; it never substitutes
unknown or trusts a supplied revision string. The existing
TAMA_HOOK_SOURCE_REVISION and TAMA_HOOK_SOURCE_DIRTY
variables, when present, are checked expectations, not the source of the fields.
Script-only releases without a native manifest derive identity directly from Git.
From a tama checkout, after release/build.sh's copy of the hook
directories into a prepared release, the maintainer flow is:
rust/target/release/tama hooks stage --source-root . --release-root <release> --cargo "$(command -v cargo)"
rust/target/release/tama hooks seal --source-root . <release>
rust/target/release/tama hooks digest <release>
A missing Git revision reports the failed Git operation and checkout path.
Hook source changed after staging or
Staged hook source differs means the candidate must be staged again,
not labeled with the checkout's newer revision. A native manifest without
sourceIdentity is also refused with restage the release.
An expectation that disagrees with Git reports both expected and observed values.
The real sealer/Git tests live under Tests/release/ and retain their
commands, outputs, exit statuses and manifests under
.build/release-evidence/.
#Installation
tama hooks install-release (the release's own bin/tama, run by the
desktop's explicit install action) takes --release, --home, and either
--session-control-only or --emergency-manifest. It passes two integrity
gates and refuses at either:
- Bundled gate. The release tree must digest to its recorded
releaseId— otherwiseBundled Tama hook release failed its integrity check. - Installed gate. The release is copied content-addressed to
~/Library/Application Support/Tama/hooks-runtime/releases/<release-id>; a pre-existing copy that no longer digests to its directory name is deleted and recopied, and a copy that still fails afterwards aborts withInstalled Tama hook release failed its integrity check.
Before any managed write it resolves Node.js from PATH,
/opt/homebrew/bin, /usr/local/bin, or ~/.local/bin, requires major
version 20 or newer, canonicalizes the executable through symlinks, and
pins it into every Node-based hook command with
shared-hooks/node-runtime-preflight.cjs preloaded. It rewrites every
registry path from the canonical source root and home (identified by the
registry's own maintainedIn and adapter paths) to the target home, stamps
the registry with the releaseId, and recomputes catalogChecksum.
The write set is transactional — every touched file is backed up first and restored on failure:
~/.shared-hooks,~/.claude/hooks,~/.codex/hooks— hook sources and the rewrittenregistry.json;~/.claude/settings.json,~/.codex/hooks.json— the provider configs, mode0600. Their other keys are preserved; thehookskey is replaced by the block the sealed CLI in the release prints for the registry being installed (tama provider config), one command per catalogued hook, each running the pinned Node plus the installed~/.shared-hooks/run-hook.mjsunderTAMA_ONLY_HOOK_ID. Generation failing, or producing an empty block, refuses the install before the transaction opens withProvider config generation failed for <provider>: <detail>; a release with no dispatcher refuses withApproved hook release is missing the Tama hook dispatcher;- the hook file of every other harness Tama declares an adapter for and this
machine has —
~/.gemini/settings.json,~/.qwen/settings.json,~/.factory/hooks.json,~/.codebuddy/settings.json,~/.cursor/hooks.json(only when it exists),~/.copilot/settings.json,~/.codeium/windsurf/hooks.json, the Tama region of~/.kimi-code/config.tomland Devin CLI's~/.config/devin/config.json— mode0600, with the blocktama provider configprints for that harness in the shape its file holds. A harness whose directory is absent is not written to. Every command routes through the same dispatcher, so the emergency state stops these harnesses' hooks as it stops Claude Code's and Codex's; ~/.local/bin/tama-agent— the generated supervisor launcher, exporting the pinned Node executable and preflight;pre-commitandpre-pushdispatchers at the user-global Git hooks path (git config --global core.hooksPath, defaulting to~/.config/git/hooks), each running the repository's own.githooks/<name>, else the archived per-repository hook fromhooks-runtime/current, then any preserved<name>.before-tamabackup;- approved
repo-githooksinto each matching project's.githooks/.
The hooks-runtime/current symlink is replaced atomically only inside the
transaction; on failure the previous symlink and every original file come
back, and an OMP extensions change (registering
~/.shared-hooks/omp-shared-hooks.js via omp config set extensions) is
reverted. Obsolete files from the previous install are deleted only when
they sit under the managed roots — anything else aborts the install.
Success writes hooks-runtime/installed.json, schema
ai.wisent.tama.installed-hook-release.v1: releaseId, catalogChecksum,
packageVersion, catalogVersion, catalogUpdatedAt, installedAt,
previousReleaseId, sourceFiles (every managed path), nodeExecutable,
nodeVersion and okoAnnouncer. That file is what the desktop reads as the installed
identity.
The install also names Tama an announcer in Oko with oko asks announcers add tama, asking
the Oko every hook asks (TAMA_OKO_CLI, else oko on PATH): the OMP
adapter's grant window only tells the operator through Oko where the grant is answered, so Oko records
that ask and delivers it on its own, and the grant is applied as soon as the window answers instead of
waiting on Oko's channels. okoAnnouncer is {named: true, command}, or
{named: false, command, cause} with the cause also on stderr. An Oko that refuses or cannot
start says tama: the operator's grant window will wait on Oko's channels until Tama is an announcer
in Oko: <cause>; a machine without Oko adds oko: "absent" and says
tama: Tama is not named an announcer in Oko: no Oko is installed here (<program> is not an
executable file or on PATH) …. Either way the install is kept.
The either/or is load-bearing. A full install replaces what an emergency
bypass set aside, so it needs that bypass's manifest; without one it refuses
with Full hook installation requires an emergency manifest and writes
nothing. Nothing in the product writes a manifest for a machine that has never
been in the bypass, so the installer is not the operator's entry point - the
bundled switch is.
#Installing a staged release on this machine
Point the switch at the release and take the machine through the bypass and
back. Disable records what it set aside and installs the session controllers
from that release; enable performs the full install with that manifest, restores
both provider configs, and repoints current:
cd ~/Documents/CodingProjects/Wisent/tama-desktop
TAMA_HOOK_RELEASE_ROOT=$PWD/.build/hook-release TAMA_EMERGENCY_ACTION=disable sh Scripts/emergency_disable_hooks
TAMA_HOOK_RELEASE_ROOT=$PWD/.build/hook-release TAMA_EMERGENCY_ACTION=enable sh Scripts/emergency_disable_hooks
Enable ends with Enabled approved hook release <first-12-of-id>, and
tama validate then reports no ERROR install drift: the provider configs
an install writes come from the sealed CLI and the registry beside it, so they
carry every catalogued hook for every event the provider supports. Running
shared-hooks/generate-configs.mjs --apply afterwards is not part of this
route; it writes a checkout's own narrower wiring over what the install just
produced.
The real native-provider installation journey is the repository's
harness_install Cargo target. It installs a genuine sealed release
over recorded native configuration in an isolated Cargo-build-directory home,
checks preservation, installed coverage and repeat installation, then verifies
that invalid configuration is refused without replacing installed state.
Its --session-control-only mode still writes native provider
configuration; it connects no Git entrypoint that was not connected, and
rewrites each global Git dispatcher that is already Tama's for the release
it activates, since that dispatcher runs the activated release's hooks. Inputs and execution
requirements are documented in
provider configuration.
#Verifying a release by hand
tama hooks integrity answers which file drifted, not just that one
did, and writes nothing:
tama hooks integrity --release <root>— recomputes the release's tree digest, prints recorded vs actual, and lists every file written after the seal;tama hooks integrity --home <home>— digests every tree underhooks-runtime/releases/against its directory name and marks thecurrentone.
Never edit files inside a release directory; the recovery for integrity failure is reinstalling the same verified app artifact (operations).