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

EventFires whenRuns in
after-new-windowA window is createddaemon
after-close-windowA window closes, by keybinding or because its process exiteddaemon
after-focus-changeFocus moves to a different windowdaemon
after-workspace-switchThe session switches to a different workspacedaemon
after-agent-stateAn agent in a pane changes state, filtered by the [notifications.agent] policydaemon
after-attachA client attaches to a session, including a switch between sessionsclient
after-detachA client detaches from a sessionclient
after-layout-changeThe tiling layout changesclient
after-resizeA window is resized, by keyboard or by dragging its borderclient

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.

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.

VariableMeaning
TUIOS_EVENTThe event name
TUIOS_WINDOW_IDWindow ID
TUIOS_WINDOW_NAMEWindow name or title
TUIOS_WORKSPACEWorkspace number
TUIOS_SESSION_IDSession name. Empty in a standalone TUIOS
TUIOS_PREV_WORKSPACEThe workspace you came from
TUIOS_LAYOUTbsp, master-stack, scrolling or floating
TUIOS_WIDTH, TUIOS_HEIGHTThe window's new size in cells
TUIOS_AGENT_STATEThe agent's new state
TUIOS_AGENT_PREV_STATEThe state it left
TUIOS_AGENT_HARNESSThe detected harness, for example claude-code
TUIOS_AGENT_MESSAGEThe last message the agent reported

Which variables carry data for each event:

EventVariables
after-new-window, after-close-window, after-focus-changeTUIOS_WINDOW_ID, TUIOS_WINDOW_NAME, TUIOS_WORKSPACE, TUIOS_SESSION_ID
after-workspace-switchTUIOS_WORKSPACE, TUIOS_PREV_WORKSPACE, TUIOS_SESSION_ID
after-attach, after-detachTUIOS_WORKSPACE, TUIOS_SESSION_ID
after-layout-changeTUIOS_LAYOUT, TUIOS_WORKSPACE, TUIOS_SESSION_ID
after-resizeTUIOS_WINDOW_ID, TUIOS_WINDOW_NAME, TUIOS_WIDTH, TUIOS_HEIGHT, TUIOS_WORKSPACE, TUIOS_SESSION_ID
after-agent-stateTUIOS_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-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:

[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\""

On this page