Skip to main content

typed-agent-hooks

typed-agent-hooks defines executable, code-first hooks for OpenAI Codex and Anthropic Claude Code. A hook's Python file owns its handlers, provider configuration, dependency environment, and executable entry point. There is no TOML manifest, import-string loader, or global installation CLI.

The package provides strict provider wire models, a conservative shared semantic API, preservation-oriented config reconciliation, ordered collections of independent hook executables, and an optional FastMCP bridge.

One executable hook

Put the dependencies in the executable with PEP 723 and define configuration beside the handler:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "cyclopts>=4.23.2,<5",
#   "pydantic-core",
#   "typed-agent-hooks==0.1.0",
# ]
# ///
"""Add local project context to every submitted prompt."""

import json
from functools import partial

from cyclopts import App
from pydantic_core import to_jsonable_python
from typed_agent_hooks import shared

cli = App(result_action=[partial(json.dumps, default=to_jsonable_python, allow_nan=False), print, "return_zero"])
hooks = shared.HookApp(name="project-context")


@hooks.on(shared.events.PromptSubmitted, timeout=10)
def add_project_context(event: shared.events.PromptSubmitted) -> shared.outputs.Result:
    """Return context derived from one submitted prompt."""

    return shared.outputs.AddContext(text=f"Working directory: {event.context.cwd}")


@cli.command
def preview(prompt: str) -> str:
    """Preview the domain behavior without constructing a provider payload."""

    return f"Would add context for {prompt!r}"


if __name__ == "__main__":
    hooks.main(cli)

Create and commit the adjacent script lockfile with uv lock --script project_context.py. Pin the desired TAH release in the PEP 723 declaration and refresh the lock when intentionally updating it; ordinary hook invocations then reuse the resolved installation.

Make the file executable and run its ordinary domain CLI directly:

./project_context.py preview "fix the tests"

hooks.main(cli) leaves ordinary arguments untouched for Cyclopts. Generated provider commands use a private _typed-agent-hooks protocol to invoke the same hooks object. That protocol is an implementation boundary, not a user-facing CLI.

The Python API is direct:

tool = ipi.import_path("/path/to/project_context.py")
tool.preview("fix the tests")
tool.hooks.render("codex", executable=tool.__file__)

Keep docstrings on public functions. Cyclopts uses them for CLI help, and notebook callers get the same documentation from Python.

Registration metadata

HookApp.on accepts shared defaults plus explicit provider-only overrides:

@hooks.on(
    shared.events.ToolCallProposed,
    timeout=20,
    status_message="Checking tool call",
    codex=shared.CodexOptions(matcher="Bash"),
    claude_code=shared.ClaudeCodeOptions(matcher="Bash|Read"),
)
def check_tool(event: shared.events.ToolCallProposed) -> shared.outputs.Result:
    ...

An app declares its enabled providers when necessary:

hooks = shared.HookApp(name="codex-only", providers=("codex",))

Shared events that do not exist on an enabled provider fail during rendering. Provider-specific options on a disabled provider also fail. The library does not guess a lossy translation.

For direct programmatic installation, call HookApp.install or HookApp.uninstall. Pass the executable explicitly so the Python call has the same information as the provider config:

hooks.install(executable=__file__, provider="all", scope="project")

Installation replaces only commands marked with the app's stable name, preserves unrelated JSON fields and hooks, writes atomically, and reconciles every selected provider. Disabling a provider therefore removes that app's stale entries from the provider config.

Ordered collections

Use a small executable installer when several hooks have independent dependencies or ownership:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "typed-agent-hooks==0.1.0",
# ]
# ///
"""Install this repository's hooks in deterministic order."""

from pathlib import Path

from typed_agent_hooks import Collection

root = Path(__file__).parent
hooks = Collection(
    name="project-hooks",
    apps=(root / "first.py", root / "second.py"),
)

if __name__ == "__main__":
    hooks.main()

The CLI and Python API are the same bound methods:

./install.py install --scope project
./install.py uninstall --scope project
installer = ipi.import_path("/path/to/install.py")
installer.hooks.install(scope="project")

Collection installation executes each child in its own PEP 723 environment, validates every description before writing anything, preserves declared order, rejects duplicate names and paths, and removes members deleted from the collection.

FastMCP forwarding

typed_agent_hooks.fastmcp.attach connects a running FastMCP server to a normal hook application. ForwardingHooks installs one forwarding command for every native event supported by each provider:

from typed_agent_hooks.fastmcp import ForwardingHooks, attach

attach(server, hooks, provider="codex", server_name="ipi")

forwarding = ForwardingHooks(
    name="ipi",
    server_name="ipi",
    timeout=70,
    startup_wait=30,
    response_timeout=35,
)
forwarding.install(provider="all", scope="user")

The dedicated tah-fastmcp-forward entry point is also a direct Cyclopts view of the importable forward function:

tah-fastmcp-forward - --provider codex --server-name ipi
from typed_agent_hooks.fastmcp import forward

output = forward(payload, provider="codex", server_name="ipi")

- means stdin only at the CLI boundary. The forwarder is fail-open: an absent, slow, dead, unsupported, or ambiguous local bridge returns no output and does not block the harness. Invalid explicit timeout arguments still fail before forwarding.

The bridge requires the fastmcp extra. The forwarding subprocess itself imports Cyclopts and the small TAH rendezvous modules, but does not import FastMCP.

Provider schemas

Provider-native schemas remain available from typed_agent_hooks.codex and typed_agent_hooks.claude_code. Wire inputs are tolerant readers: unknown provider fields are ignored while declared fields remain strictly typed. Outputs are closed and exact. Use the shared API when one semantic handler is valid for both providers; use the provider-native models when their behavior genuinely differs.

Development

uv run ruff check .
uv run ty check
uv run pytest -q

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typed_agent_hooks-0.1.0.tar.gz (43.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typed_agent_hooks-0.1.0-py3-none-any.whl (59.1 kB view details)

Uploaded Python 3

File details

Details for the file typed_agent_hooks-0.1.0.tar.gz.

File metadata

  • Download URL: typed_agent_hooks-0.1.0.tar.gz
  • Upload date:
  • Size: 43.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for typed_agent_hooks-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8bbbe49cb8b609c6bd77ae0a816dd8984e57065c5b1fd3b027241ef0e0314def
MD5 c65fec1a29d71ce584b029d00a2fffd3
BLAKE2b-256 6bf1674d5de6c4ba7563af5d393bcd35762eff8eb6b9ee0c681c2e1e5cf2b294

See more details on using hashes here.

Provenance

The following attestation bundles were made for typed_agent_hooks-0.1.0.tar.gz:

Publisher: release.yml on nimashoghi/typed-agent-hooks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file typed_agent_hooks-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for typed_agent_hooks-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4318a12735d96632d41fbc831687ede908ebccf75d1bc0cd2189a2f07e18aa6a
MD5 431edcc2e8023bdedce933235284750f
BLAKE2b-256 5e57de619da467ee55e10d94ae5fb1c5d245e69d6e55579c5eb0bf426d6ad845

See more details on using hashes here.

Provenance

The following attestation bundles were made for typed_agent_hooks-0.1.0-py3-none-any.whl:

Publisher: release.yml on nimashoghi/typed-agent-hooks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page