# Hooks

URL: https://tuios.dev/docs/hooks

> Run shell commands when windows, workspaces, sessions and agents change.

A hook is a shell command TUIOS runs when something happens: a window opens, focus moves, you switch workspace, you attach or detach, an agent finishes. Hooks live in the `[hooks]` table of your [config file](https://tuios.dev/docs/configuration).

```toml
[hooks]
after-new-window = "notify-send 'TUIOS' \"opened $TUIOS_WINDOW_NAME\""
after-workspace-switch = "echo \"$TUIOS_PREV_WORKSPACE -> $TUIOS_WORKSPACE\" >> ~/.tuios-ws.log"
```

> **Event names use hyphens**
>
> Other config keys use `snake_case`. Hook event names use hyphens and are case-sensitive: `after-new-window`, not `after_new_window`. An unknown name is not an error. TUIOS logs one line and the hook never runs. `tuios list-hooks` shows what was actually loaded.

## Events

| Event                    | Fires when                                                                       | Runs in |
| ------------------------ | -------------------------------------------------------------------------------- | ------- |
| `after-new-window`       | A window is created                                                              | daemon  |
| `after-close-window`     | A window closes, by keybinding or because its process exited                     | daemon  |
| `after-focus-change`     | Focus moves to a different window                                                | daemon  |
| `after-workspace-switch` | The session switches to a different workspace                                    | daemon  |
| `after-agent-state`      | An agent in a pane changes state, filtered by the `[notifications.agent]` policy | daemon  |
| `after-attach`           | A client attaches to a session, including a switch between sessions              | client  |
| `after-detach`           | A client detaches from a session                                                 | client  |
| `after-layout-change`    | The tiling layout changes                                                        | client  |
| `after-resize`           | A window is resized, by keyboard or by dragging its border                       | client  |

A hook runs on the side that owns the fact it reports:

- **Daemon events** describe the session. The daemon runs them, so they fire when nobody is attached, and they fire once however many clients are attached.
- **Client events** describe one client. Each attached client runs them from its own config. Three clients attaching is three `after-attach` firings.
- A standalone TUIOS (`tuios --standalone`) has no daemon and runs every hook itself.

`after-agent-state` follows the same `[notifications.agent]` settings as the other agent alerts, including the settle delay, so it does not fire on every brief change. `suppress_focused` only applies while a client is attached. A command in `[notifications.agent].command` is registered as one more `after-agent-state` hook. See [Notifications](https://tuios.dev/docs/configuration#notifications).

## One command or several

A value is one string, or an array of strings:

```toml
[hooks]
after-attach = "my-greeting"

after-new-window = [
  "logger -t tuios \"new window $TUIOS_WINDOW_ID\"",
  "touch ~/.cache/tuios/last-window",
]
```

The commands in an array run at the same time, not in order. If order matters, put the steps in one string joined with `&&`.

## What a hook receives

TUIOS runs the string with `sh -c`. There are no arguments and no stdin. Everything arrives as environment variables.

Every variable is set for every event. A value that does not apply is empty for text and `0` for numbers, so a script can read any of them without checking first.

| Variable                      | Meaning                                          |
| ----------------------------- | ------------------------------------------------ |
| `TUIOS_EVENT`                 | The event name                                   |
| `TUIOS_WINDOW_ID`             | Window ID                                        |
| `TUIOS_WINDOW_NAME`           | Window name or title                             |
| `TUIOS_WORKSPACE`             | Workspace number                                 |
| `TUIOS_SESSION_ID`            | Session name. Empty in a standalone TUIOS        |
| `TUIOS_PREV_WORKSPACE`        | The workspace you came from                      |
| `TUIOS_LAYOUT`                | `bsp`, `master-stack`, `scrolling` or `floating` |
| `TUIOS_WIDTH`, `TUIOS_HEIGHT` | The window's new size in cells                   |
| `TUIOS_AGENT_STATE`           | The agent's new state                            |
| `TUIOS_AGENT_PREV_STATE`      | The state it left                                |
| `TUIOS_AGENT_HARNESS`         | The detected harness, for example `claude-code`  |
| `TUIOS_AGENT_MESSAGE`         | The last message the agent reported              |

Which variables carry data for each event:

| Event                                                          | Variables                                                                                                                                               |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `after-new-window`, `after-close-window`, `after-focus-change` | `TUIOS_WINDOW_ID`, `TUIOS_WINDOW_NAME`, `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID`                                                                           |
| `after-workspace-switch`                                       | `TUIOS_WORKSPACE`, `TUIOS_PREV_WORKSPACE`, `TUIOS_SESSION_ID`                                                                                           |
| `after-attach`, `after-detach`                                 | `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID`                                                                                                                   |
| `after-layout-change`                                          | `TUIOS_LAYOUT`, `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID`                                                                                                   |
| `after-resize`                                                 | `TUIOS_WINDOW_ID`, `TUIOS_WINDOW_NAME`, `TUIOS_WIDTH`, `TUIOS_HEIGHT`, `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID`                                            |
| `after-agent-state`                                            | `TUIOS_WINDOW_ID`, `TUIOS_WINDOW_NAME`, `TUIOS_AGENT_STATE`, `TUIOS_AGENT_PREV_STATE`, `TUIOS_AGENT_HARNESS`, `TUIOS_AGENT_MESSAGE`, `TUIOS_SESSION_ID` |

A hook the daemon runs gets the daemon's environment, not your current shell's. A daemon started by `tuios new` keeps the environment of the shell that started it. Use full paths when in doubt.

## When a hook does not fire

```bash
tuios list-hooks
```

This lists every registered command with the side that runs it, how many times it ran, its last exit code, when it last ran and its last error. The error is the tail of the command's stderr.

- **No row**: the hook was never loaded. Check the event name.
- **0 runs**: the command is registered and the event has not happened.
- **Non-zero exit**: the command ran and failed. The error says why.

`tuios list-hooks --event after-close-window` shows one event, and `--json` gives machine-readable output. Client hooks are listed only while a client is attached.

The daemon also logs a warning for every failing hook. Run `tuios daemon --log-level=basic` to log one line per firing as well.

## Limits

- **Hooks load at startup.** Editing `[hooks]` does not reload them. Restart the client for client hooks, and run `tuios kill-server` so the daemon restarts for daemon hooks. This stops every session and the processes in them. The next `tuios` restores the saved layouts, but not the running programs.
- **No timeout.** A hook that never exits keeps running. On detach, and when the daemon shuts down, TUIOS waits at most 2 seconds for running hooks.
- **Stdout is discarded.** Only the tail of stderr (1 KiB) and the exit code are kept, for `list-hooks`. Redirect to a file if you need the output.
- **No ordering between hooks.** Each command runs in its own goroutine, so do not rely on the order of two events that happen together.
- **Restored windows count as new.** When the daemon restores a saved session, `after-new-window` fires for each window it brings back.
- **`after-detach` needs a daemon session.** A standalone TUIOS has nothing to detach from.
- **Wrong types are ignored.** A number or a boolean where a string or array belongs is dropped without a message.

## Examples

Log every event, to see what fires and when:

```toml
[hooks]
after-new-window = "echo \"$TUIOS_EVENT $TUIOS_WINDOW_ID $TUIOS_WINDOW_NAME\" >> ~/.tuios-hooks.log"
after-close-window = "echo \"$TUIOS_EVENT $TUIOS_WINDOW_ID\" >> ~/.tuios-hooks.log"
after-focus-change = "echo \"$TUIOS_EVENT $TUIOS_WINDOW_NAME\" >> ~/.tuios-hooks.log"
after-workspace-switch = "echo \"$TUIOS_EVENT $TUIOS_PREV_WORKSPACE->$TUIOS_WORKSPACE\" >> ~/.tuios-hooks.log"
after-layout-change = "echo \"$TUIOS_EVENT $TUIOS_LAYOUT\" >> ~/.tuios-hooks.log"
after-resize = "echo \"$TUIOS_EVENT ${TUIOS_WIDTH}x${TUIOS_HEIGHT}\" >> ~/.tuios-hooks.log"
```

Show attach state in a status bar:

```toml
[hooks]
after-attach = "echo attached > ~/.cache/tuios-state"
after-detach = "echo detached > ~/.cache/tuios-state"
```

Send a desktop notification when an agent needs you:

```toml
[hooks]
after-agent-state = "[ \"$TUIOS_AGENT_STATE\" = needs_input ] && notify-send \"$TUIOS_WINDOW_NAME needs input\""
```

## Related

*[An interactive figure goes here. Open the page to use it.](https://tuios.dev/docs/hooks)*
