hook
Shell hook handler invoked by shell config, not by users directly.
Hidden from --help. hook is implemented, tested, and shipped in every
release; it just isn't meant to be typed by a person. treeship install
writes calls to it into your shell config, and your shell calls it on every
command. It stays out of --help so it doesn't show up as something to run
directly.
treeship hook pre and treeship hook post are what treeship install's
shell integration actually calls. Each matches the command that just ran (or
is about to run) against the project's attest.commands[] rules
(.treeship/config.yaml, the same rules a template
sets up) and, on a match, signs an action receipt without you typing
treeship wrap.
How your shell calls it
treeship install appends a snippet like this to ~/.zshrc / ~/.bashrc /
~/.config/fish/config.fish:
# zsh
treeship_preexec() {
TREESHIP_LAST_CMD="$1"
treeship --quiet hook pre -- "$1" 2>/dev/null
}
add-zsh-hook preexec treeship_preexec
treeship_precmd() {
treeship --quiet hook post "$?" -- "${TREESHIP_LAST_CMD-}" 2>/dev/null
unset TREESHIP_LAST_CMD
}
add-zsh-hook precmd treeship_precmdbash uses a DEBUG trap plus PROMPT_COMMAND; fish uses --on-event fish_preexec / fish_postexec. All three call the same two subcommands,
and all three hand the finished command to hook post so state left by a
pre whose post never ran is never charged to the next command. In bash
the DEBUG trap fires for every simple command, so the snippet acts only on
the first one after a prompt and takes the whole line from history when
history just recorded it (git commit -m x && echo ok arrives as one line,
as it does in zsh and fish); a line history did not record (ignorespace,
set +o history) is taken from BASH_COMMAND, never from the previous
history entry. The hooks run with --quiet, so a matched command prints
nothing into the prompt. The bash snippet installs its own DEBUG trap, replacing one
you had set. Hooks written by a release before 0.31.11 pass no command;
treeship install replaces such a block with the current one, changing
only the bytes between its two marker lines.
treeship hook pre
treeship hook pre <COMMAND>| Argument | Description |
|---|---|
<COMMAND> | The command about to run |
Looks up the project config (walking up from the current directory for a
.treeship/config.yaml next to a .treeship/config.json -- both must be
present, so a cloned repo's config.yaml alone is never trusted). If no
project config is found, or no attest.commands[] pattern matches, it exits
silently and does nothing.
On a match:
- If the matched rule sets
require_approval: trueand the command hasn't already been approved (treeship approve), it prints the pending-approval hint and exits 1. - Otherwise it redacts secrets from the command line and writes a
.pending_hookstate file forhook postto pick up.
hook pre's exit code is not acted on by any of the installed shell
snippets (treeship install's zsh preexec, bash DEBUG trap, or fish
fish_preexec hook): none of them abort the command that's about to run on
a non-zero exit. The hook records; it cannot cancel a command. The pending
state it writes is a record for hook post and the local approval workflow,
not an enforcement point.
treeship hook post
treeship hook post <EXIT_CODE> [COMMAND]| Argument | Description |
|---|---|
<EXIT_CODE> | Exit code of the completed command |
[COMMAND] | The command that ran (optional; the shell snippets pass it after --) |
Reads .pending_hook (written by pre), and does nothing if it isn't there
-- meaning pre didn't match a rule for this command. Deletes the state
file immediately so a crash doesn't leave stale state. When a command is
given and it is not the one pre matched, the state was stale (that pre's
own post never ran) and nothing is recorded; without a command, state
older than 24 hours is dropped the same way. Otherwise it signs the action
receipt (elapsed time, exit code, the matched label) and writes .last.
Failure mode
Both subcommands fail open: no project config, no matching rule, or any
internal error, and the shell command you actually ran proceeds unaffected
(2>/dev/null on the shell side hides hook errors from your terminal too).
Nothing about hook blocks your shell except the explicit require_approval
exit-1 case above.