PLUGINS: THE COMMAND HOOK

The terminal ships no policy. You bring the judgment.

No terminal can decide by design that curl x | sh or a subtly wrong rm is dangerous - that is a judgment call, not a parsing rule. So secure-terminal keeps judgment out of the terminal and gives you one opt-in extension point: a command hook. Before a command runs in CLI mode, the terminal can call an external handler you configure, which may allow it, warn, suggest a safer command, or block it. The terminal ships no AI and no rules of its own; the logic lives entirely in your handler.

A tiny JSON protocol

The handler is any executable. It reads a JSON request on stdin and writes a JSON verdict on stdout. That is the whole interface.

# request (stdin): the command you are about to run {"version":1,"command":"git push --force","cwd":"/home/user/repo","tab":"repo"} # verdict (stdout): one of allow | block | ask | need_transcript {"verdict":"ask","message":"Force-push rewrites history.","suggestion":"git push --force-with-lease"}

Four verdicts. allow runs the command; block refuses it with your message; ask shows your message and makes you confirm; a suggestion, if present, is offered as a safer command (sanitised before it can reach the shell). The fourth, need_transcript, is the interesting one.

Cheap first, escalate only if needed

The scrollback is long, expensive to process, and the most injection-prone input there is. So it is not sent unless your handler asks.

On the first call the handler sees only the command. If it can decide from that (most commands), it answers allow/block/ask and you are done. If it needs context, it replies need_transcript, and secure-terminal calls it a second time with the recent scrollback attached. A local rules handler almost never needs it; an AI handler asks for it only on the commands that are ambiguous without context.

Two example handlers

secure-terminal ships two samples under /usr/share/secure-terminal/hooks/. Point the terminal at one, or write your own.

example-hook - local rules, no network

A fast, dependency-free handler using only local pattern rules: it blocks a downloaded script piped straight into a root shell, and asks before curl | sh and git push --force. Fully offline, milliseconds per call. A good starting point to copy and edit.

ai-judge-hook - ask a model

Pipes the command (and, when it escalates, the scrollback) to a model you choose - claude --print by default, any command via SECURE_TERMINAL_AI. Crucially, the command and transcript are framed to the model as untrusted data to be judged, never as instructions, so a hostile log line cannot talk the judge into approving itself. It fails open with a visible note if the model is unavailable, and has a fast path so trivial commands never hit the network.

Enable it

Off by default. Turn it on with a settings drop-in.

# ~/.config/secure-terminal.d/50_user.conf command_hook=/usr/share/secure-terminal/hooks/example-hook command_hook_on_error=allow # or 'block' to fail closed command_hook_transcript=none # 'ask' lets the handler request scrollback

Write your own handler in any language: read the JSON request from stdin, print a JSON verdict to stdout, exit 0. Keep the policy in the handler, where you can read and change it - not baked into the terminal.

Why in the terminal, not the shell?

A fair question. zsh's preexec or bash-preexec can call a handler before each command too, and for most people that is simpler and sees more: it runs inside your real shell, after alias and variable expansion. If that fits you, use it - secure-terminal does not need to own this, and most of what the hook does, a shell preexec does as well.

The terminal hook earns its place in exactly two cases a shell hook cannot cover:

A guard the command can't switch off

A shell preexec is shell state: a command can unset -f it, redefine it, exec a different shell, or run in a subshell that never loaded it. The terminal hook runs outside the shell, so the command it is about to judge cannot disable the judge first.

Programs with no hook of their own

In CLI mode the terminal does the line editing, so it can gate input to any line-oriented program - dash, a Python or psql REPL - none of which have a preexec to hang a guard on.

Both come with costs, stated plainly: the hook is fail-open by default (a convenience speed bump, not a hard boundary - set command_hook_on_error=block to fail closed), it runs in CLI mode only, and it sees the line you typed, not what the shell expands it to. If none of those two edges matter to you, a shell preexec is the lighter choice - and either way, the terminal's real, by-design guarantees are safe display and safe paste, not this.

Limits

The hook sees the command line you typed, not what the shell expands it to (aliases, variables, globs), and it runs in CLI mode only - a shell prompt inside a full-screen TUI program is not hooked. It is a deliberate speed bump for the commands you type, not a sandbox: it cannot stop a program you have already chosen to run from doing what it does. For what the terminal can guarantee by design - safe display and safe paste - see the comparison.