Gates check the work. Hooks and walls are about the run: hooks are told what is happening, walls decide what may happen.

Hooks

A hook is a file droost runs when something happens in a run: post to chat, update a dashboard, write a log.

yaml
hooks:
  run_start:    droost/hooks/notify.php
  phase_begin:  droost/hooks/notify.php
  phase_end:    droost/hooks/notify.php
  run_complete: droost/hooks/notify.php

It gets JSON on standard input with phase, run_id and work_item (the ticket, if there is one), and DROOST_PROJECT_ROOT in the environment.

php
<?php
// droost/hooks/notify.php: one line per event, to a log.
$event = json_decode(stream_get_contents(STDIN), true);
file_put_contents(getenv('DROOST_PROJECT_ROOT') . '/droost/runs.log',
  date('c') . " {$event['run_id']} {$event['phase']}\n", FILE_APPEND);
echo "logged\n";

The first line it prints is shown to the agent. A hook can never stop or fail a run.

Walls

A wall is asked before the agent uses a tool whose name matches a pattern. It lets the call through or refuses it, with a reason the agent sees.

yaml
walls:
  tracker_writes: { tools: "^mcp__atlassian__(create|edit|transition)", run: droost/walls/tracker-writes.php }

It gets JSON on standard input with tool_name, input (the arguments) and project_root. Exit 0 allows the call; exit 2 refuses it, and what you print on standard error is the reason. A wall that crashes, is missing or takes longer than 10 seconds refuses: it never says yes by accident.

php
<?php
// droost/walls/tracker-writes.php: the agent may read the tracker, not write to it.
$call = json_decode(stream_get_contents(STDIN), true);
fwrite(STDERR, "Ask a person to make this change: {$call['tool_name']}\n");
exit(2);

A wall is asked about file edits, shell commands, MCP tools (mcp__…) and the agent's questions to you: the calls the guard sees. It is not asked about other tools, such as reading a file or fetching a web page. Walls work where the guard does, which today means Claude Code.

Scripts only a person may run

List a script under operator_only: and the guard refuses it from the agent, and tells the agent to hand it to you:

yaml
operator_only:
  - droost/bin/publish-release

Panel tabs

Add a tab of your own to the run panel: a script that prints JSON.

yaml
panel:
  tabs:
    release: { title: Release, run: droost/panel/release.php }

It gets project_root, tab and run (the open run, or null) on standard input, and prints one object:

json
{ "badge": "2/3", "sections": [
  { "kind": "checklist", "title": "Before release", "items": [
    { "text": "Changelog written", "done": true },
    { "text": "Config exported", "done": true },
    { "text": "Translations updated", "done": false, "note": "fr missing" } ] } ] }

A section's kind is text, markdown, fields (items: [{label, value}]), checklist (items: [{text, done, note?}]) or list (items: [{text, detail?, tag?, tone?}]). A .ts or .js file runs under Node. droost workflow:panel --json shows the whole panel, your tabs included.