Docs/Reference

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.

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:

MemberContent
release.jsonThe 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.jsonApproved hook sources that live outside the managed roots, packaged in with sourcePrefix → releasePath mappings (schema ai.wisent.tama.external-hook-sources.v1)
bin/tamaThe 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.jsonThe 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:

  1. Bundled gate. The release tree must digest to its recorded releaseId — otherwise Bundled Tama hook release failed its integrity check.
  2. 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 with Installed 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 rewritten registry.json;
  • ~/.claude/settings.json, ~/.codex/hooks.json — the provider configs, mode 0600. Their other keys are preserved; the hooks key 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.mjs under TAMA_ONLY_HOOK_ID. Generation failing, or producing an empty block, refuses the install before the transaction opens with Provider config generation failed for <provider>: <detail>; a release with no dispatcher refuses with Approved 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.toml and Devin CLI's ~/.config/devin/config.json — mode 0600, with the block tama provider config prints 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-commit and pre-push dispatchers 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 from hooks-runtime/current, then any preserved <name>.before-tama backup;
  • approved repo-githooks into 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 under hooks-runtime/releases/ against its directory name and marks the current one.

Never edit files inside a release directory; the recovery for integrity failure is reinstalling the same verified app artifact (operations).