# peer_hooks


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

Claude Code and Codex have no control socket, as Ramabana has. They run
hooks: commands that the agent starts at fixed events. Every peer hook
is one command, `PYTHON -m ramabana.peer_hooks KIND EVENT`. `PYTHON` is
the full path of the Python that ran `ramabana setup peers`, so the hook
does not depend on the `ramabana` that is first on `PATH`; an older
`ramabana` there would read the words as its own options and fail. It
reads the hook input as JSON on stdin and writes what the agent expects.

- `session-start` registers the session and tells the agent its peer
  name, the live peers and how to send.
- `prompt` gives the agent its unread messages when the user types. It
  also ends work together mode, because the user is back, and shows the
  report of the mode.
- `stop` keeps the agent going while messages are unread.
- `wait` (Claude Code only) waits in the background and wakes an idle
  session when a message comes.
- `end` removes the registration.
- `pre-tool` refuses a write into a folder that another live session
  holds.

Hooks must be fast and must not break the agent. They make no model call
and use no network. An error goes to `hooks.log` in the peers folder,
and the hook then does nothing. A hook never exits with code 2, which
blocks the agent, except the waiter when it has messages. The module
does not load the agent code.

## Adapters

An
[`Adapter`](https://vedicreader.github.io/ramabana/peer_hooks.html#adapter)
holds what is different for each agent kind: its config file, the file
that gets the `peer-work` skill, the variable that holds its session id,
and whether a hook can wake it. `ramabana setup peers` uses them to
install the hooks.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_hooks.py#L22"
target="_blank" style="float:right; font-size:smaller">source</a>

### Adapter

``` python
def Adapter(
    kind:str, config:str, skill_path:str, session_env:str | None, wakes:bool
)->None:
```

*What the peer hooks need to know about one agent kind.*

[`hook_block`](https://vedicreader.github.io/ramabana/peer_hooks.html#hook_block)
gives the hook entries for one agent’s config, by event. Claude Code
gets a second `Stop` hook, the waiter: `async` runs it in the
background, and `asyncRewake` lets its exit code 2 wake an idle session.
Claude Code stops an `asyncRewake` hook at its `timeout`, so the waiter
gets a long one. Codex has no way to wake an idle session, and it names
its file edit tool `apply_patch`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_hooks.py#L47"
target="_blank" style="float:right; font-size:smaller">source</a>

### hook_block

``` python
def hook_block(
    kind
):
```

*The hook groups, by event, that agent `kind` needs in its config.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_hooks.py#L39"
target="_blank" style="float:right; font-size:smaller">source</a>

### hook_cmd

``` python
def hook_cmd(
    kind, event
):
```

*The command that runs hook `event` of agent `kind` with this Python,
whatever `ramabana` is first on `PATH`.*

Each event has one group. Claude Code has two `Stop` groups; Codex has
one.

``` python
b = hook_block('claude')
test_eq(b['PreToolUse'], [{'matcher': 'Edit|Write|NotebookEdit', 'hooks': [{'type': 'command', 'command': f'{sys.executable} -m ramabana.peer_hooks claude pre-tool'}]}])
test_eq([g['hooks'][0].get('asyncRewake') for g in b['Stop']], [None, True])
test_eq(len(hook_block('codex')['Stop']), 1)
```

## What the agent reads

[`_messages`](https://vedicreader.github.io/ramabana/peer_hooks.html#_messages)
writes unread messages for the agent with
[`peer_text`](https://vedicreader.github.io/ramabana/peers.html#peer_text).
An ask also says who decides. Outside work together mode, the agent must
ask the user first. In work together mode with that sender, it can act
without the user. The ask also shows the command that answers it, as
`PEER_RUN`: the Python the hook runs under, with `-m ramabana.peers`. An
older `ramabana` first on `PATH` may have no
[`tell`](https://vedicreader.github.io/ramabana/peers.html#tell), so the
agent does not depend on it. These lines come before the
`<peer-message>` block, as
[`peer_text`](https://vedicreader.github.io/ramabana/peers.html#peer_text)
puts them. In work together mode an ask also counts against the budget:
one over it gets the reply `refused: budget: ...` at once, and the agent
does not see it. Codex does not give its session id to commands, so its
commands carry `--session`.

## Session start

[`_start`](https://vedicreader.github.io/ramabana/peer_hooks.html#_start)
registers the session with the git root of its folder and the agent’s
process id. Only a git root is guarded: a session started outside git,
for example in your home folder, shows its folder but locks nothing.
Then it tells the agent who it is, which peers are live, and how to
send. The brief gives `PEER_RUN` once as the command to use for
`ramabana`.

## The user is back

When the user types,
[`_ended`](https://vedicreader.github.io/ramabana/peer_hooks.html#_ended)
ends work together mode. It returns the start time of a mode that ended
since the user last saw one, also a mode that ended while the user was
away. The end of a mode waits in the small file `<key>.ended`, and
[`_ended`](https://vedicreader.github.io/ramabana/peer_hooks.html#_ended)
removes it, so the user sees each end once. The hook reads only that
file on each prompt, not the whole log; a `report_shown` row in the log
keeps the record.
[`_report_note`](https://vedicreader.github.io/ramabana/peer_hooks.html#_report_note)
gives the saved report, or tells the agent to make it.

Claude Code also sends prompts that no human typed through
`UserPromptSubmit`: a background task’s notification, a subagent’s
hand-back, and a message from another session. The hook input has no
field that tells them apart from a typed prompt; it has only the
`prompt` text. Claude Code opens each such prompt with a frame: a
hand-back or a message from another session starts with
`Another Claude session sent a message:` and an `<agent-message>` or
`<cross-session-message>` block, and a notification starts with
`<task-notification>`.
[`_typed`](https://vedicreader.github.io/ramabana/peer_hooks.html#_typed)
looks for a line that opens with one of these frames, so a typed prompt
that only mentions a tag in its text still counts as typed.
[`_prompt`](https://vedicreader.github.io/ramabana/peer_hooks.html#_prompt)
ends work together mode only for a typed prompt.

## Stop and wait

[`_stop`](https://vedicreader.github.io/ramabana/peer_hooks.html#_stop)
blocks the stop while messages are unread, so the agent goes on and
reads them. The reason is the messages. It blocks only for new messages,
so it cannot loop: each message is taken once, the hop limit counts, and
Claude Code stops after 8 blocks.

[`_wait`](https://vedicreader.github.io/ramabana/peer_hooks.html#_wait)
is the background waiter. Only one runs for each session: it holds a
lock on the `.wait` file and writes its process id there. A second
waiter exits at once. A lock dies with its process, so a crashed waiter
does not block the next one. On messages, the waiter exits with code 2
and writes them to stderr, which wakes Claude. If it cannot write the
messages it took, it puts them back in the inbox. It exits with code 0
when the session ends. When work together mode expires while it waits,
it saves the report first.

## Session end and the folder guard

[`_end`](https://vedicreader.github.io/ramabana/peer_hooks.html#_end)
removes the registration, so the waiter stops. It leaves the `.wait`
file, because the waiter can hold a lock on it. Claude Code gives
`SessionEnd` only 1.5 s, so it does nothing more.

[`_pre_tool`](https://vedicreader.github.io/ramabana/peer_hooks.html#_pre_tool)
finds the paths a tool call writes. Claude Code’s file tools give
`file_path` or `notebook_path`. Codex’s `apply_patch` gives the patch,
which names each file.
[`folder_denial`](https://vedicreader.github.io/ramabana/peers.html#folder_denial)
checks each path. A refusal names the owner, and the agent can send it a
message instead.

A patch names files to add, change, delete and move to.

``` python
test_eq(_paths({'tool_name': 'apply_patch', 'tool_input': {'command': '*** Begin Patch\n*** Update File: a.py\n*** Move to: b.py\n*** Add File: /r/c.py\n*** End Patch'}}), ['a.py', 'b.py', '/r/c.py'])
test_eq(_paths({'tool_name': 'Bash', 'tool_input': {'command': '*** Add File: x'}}), [])
test_eq(_paths({'tool_input': {'notebook_path': '/r/n.ipynb'}}), ['/r/n.ipynb'])
```

## The entry point

[`peer_hook`](https://vedicreader.github.io/ramabana/peer_hooks.html#peer_hook)
runs one event and returns the exit code, stdout and stderr. An error is
logged to `hooks.log` and gives exit code 0 with no output, so a broken
hook never stops the agent.
[`main`](https://vedicreader.github.io/ramabana/cli.html#main) is
`python -m ramabana.peer_hooks`, the command in the hooks;
`ramabana peer-hook`, the command of older setups, runs it too. Wrong
words or input that is not JSON are logged and give exit code 0, never
2. The event `version` prints the ramabana version, so `--doctor` can
see which code the hooks run.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/cli.py#L3055"
target="_blank" style="float:right; font-size:smaller">source</a>

### main

``` python
def main(
    argv
):
```

*Run `python -m ramabana.peer_hooks KIND EVENT` on the hook input in
stdin; the exit code, which is 2 only from a waiter with messages.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_hooks.py#L199"
target="_blank" style="float:right; font-size:smaller">source</a>

### peer_hook

``` python
def peer_hook(
    kind, event, data, d:NoneType=None, cfg:NoneType=None
):
```

*Run hook `event` of agent `kind` on the hook input `data`: (exit code,
stdout, stderr); an error is logged and gives (0, ’‘,’’).*

A stop with nothing unread lets the agent stop. A note makes it block,
once. A write into another session’s folder is refused, and a write into
the session’s own folder is not. A broken input is logged.

``` python
from ramabana.peers import Envelope, post, register
with tempfile.TemporaryDirectory() as t, patch.dict(os.environ, RAMABANA_PEERS=t):
    s1 = {'session_id': 's1', 'cwd': t}
    test_eq(peer_hook('claude', 'stop', s1, cfg=t), (0, '', ''))
    post(t, 'claude-s1', Envelope('note', 'v2 is out', {'key': 'ramabana-r1', 'name': 'leela'}))
    assert 'v2 is out' in json.loads(peer_hook('claude', 'stop', s1, cfg=t)[1])['reason']
    test_eq(peer_hook('claude', 'stop', s1, cfg=t), (0, '', ''))
    register(t, 'codex', 'c9', ['/r/abhyasa'], os.getpid())
    peer_hook('claude', 'session-start', s1, cfg=t)
    deny = json.loads(peer_hook('claude', 'pre-tool', s1 | {'tool_name': 'Edit', 'tool_input': {'file_path': '/r/abhyasa/a.py'}}, cfg=t)[1])
    test_eq(deny['hookSpecificOutput']['permissionDecision'], 'deny')
    test_eq(peer_hook('claude', 'pre-tool', s1 | {'tool_name': 'Edit', 'tool_input': {'file_path': f'{t}/a.py'}}, cfg=t), (0, '', ''))
    test_eq(peer_hook('claude', 'stop', {}, cfg=t), (0, '', ''))
    assert 'KeyError' in (Path(t)/'hooks.log').read_text()
```
