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.
[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-attachfirings. - 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.
One command or several
A value is one string, or an array of strings:
[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
tuios list-hooksThis 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 runtuios kill-serverso the daemon restarts for daemon hooks. This stops every session and the processes in them. The nexttuiosrestores 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-windowfires for each window it brings back. after-detachneeds 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:
[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:
[hooks]
after-attach = "echo attached > ~/.cache/tuios-state"
after-detach = "echo detached > ~/.cache/tuios-state"Send a desktop notification when an agent needs you:
[hooks]
after-agent-state = "[ \"$TUIOS_AGENT_STATE\" = needs_input ] && notify-send \"$TUIOS_WINDOW_NAME needs input\""