Docs / Hooks

Lifecycle hooks

Hooks run shell commands at specific points in the worktree lifecycle. They're the mechanism for provisioning anything that isn't a port, database, or env file: registering aliases, seeding data, setting debug flags, cleaning up on exit.

Why hooks exist

GTL allocates resources (ports, databases, env files) declaratively. But some setup steps can't be expressed as config fields. You might need to run a migration, install dependencies, register a router alias, or seed test data. Hooks give you shell access at the right moment in the lifecycle without requiring you to remember a sequence of manual commands.

There are two categories of hooks, designed for different lifecycle moments.

Setup & release hooks

These run during worktree creation and teardown. They're for one-time provisioning: installing dependencies, running migrations, copying credentials, cleaning up artifacts.

Hook When it runs On failure
pre_setup Before commands.setup, after env file is written Setup aborts
post_setup After commands.setup and editor config Warns, continues
pre_release After release confirmation, before freeing resources Release aborts
post_release After resources are freed Warns, continues

These are defined as array-valued entries under the hooks: block in .treeline.yml. They sit alongside named start hooks in the same block; the parser distinguishes them by value type (arrays vs maps).

.treeline.yml
hooks:
  pre_setup:
    - cp .env.template .env.local
  post_release:
    - rm -rf tmp/cache

Triggers: gtl new, gtl setup, gtl release, gtl prune. These hooks do not run during gtl start or gtl restart.

Start hooks

Start hooks (v0.36.0) run before the server boots and after it exits. They're for per-session provisioning that needs to happen every time you start work on a branch, not just when the worktree is first created.

The canonical example: registering a router alias before the server starts so an OAuth callback URL points at the right branch, then removing it on exit. The alias is session-scoped, not permanent.

Configuration

Start hooks are named entries under the hooks: key. Each entry can have pre_start, post_stop, and auto.

.treeline.yml
hooks:
  oauth:
    pre_start: "gtl serve alias myapp-oauth {port}"
    post_stop: "gtl serve alias --remove myapp-oauth"

  seed:
    pre_start:
      - bin/rails db:seed
      - bin/rails cache:clear
    auto: true

Each pre_start and post_stop field accepts a single string or an array of strings. A hook must have at least one command in either field to be recognized. Setting auto: true alone with no commands does nothing.

Activation

Hooks are activated in two ways:

  • auto: true runs the hook on every fresh gtl start without any flag.
  • gtl start --with oauth activates a specific hook for this session. Accepts comma-separated names: --with oauth,seed.

When both are present, auto hooks run first, then --with hooks in the order specified on the command line. If a hook is both auto: true and named in --with, it runs once (deduplicated).

Note: when multiple hooks have auto: true, their relative execution order is not guaranteed. If ordering matters between auto hooks, use a single hook with multiple commands in its pre_start array.

The lifecycle, step by step

Here's what happens when you run gtl start --with oauth on a fresh start (no supervisor running):

  1. GTL resolves the active hooks: all auto: true hooks, plus any named in --with, deduplicated.
  2. Each hook's pre_start commands run in sequence. Commands are executed with sh -c in the worktree directory. Stdout and stderr are inherited (you see the output).
  3. If any pre_start command fails, the start aborts. No server process launches. The error message identifies which hook and command failed.
  4. On success, GTL writes a state file recording which hooks were activated. This file lives alongside the supervisor socket (e.g. /tmp/gtl-xxxxxxxx.hooks).
  5. The env file is regenerated, editor settings are synced, and the supervisor launches commands.start.
  6. When the supervisor exits (Ctrl+C), GTL reads the hooks state file and runs each hook's post_stop commands in reverse activation order.
  7. post_stop failures are logged to stderr but do not block shutdown. The state file is cleaned up regardless.

$ gtl start --with oauth

[hook:oauth] Running pre_start: gtl serve alias myapp-oauth 3010

Alias myapp-oauth.prt.dev → :3010

[hook:seed] Running pre_start: bin/rails db:seed

[hook:seed] Running pre_start: bin/rails cache:clear

[supervisor] Starting bin/dev...

▸ Rails booting on :3010

... working ...

^C

[supervisor] Shutting down...

[hook:seed] No post_stop defined, skipping.

[hook:oauth] Running post_stop: gtl serve alias --remove myapp-oauth

Removed alias "myapp-oauth".

Interpolation

Hook commands support {port} interpolation. The allocated port for the current worktree is substituted at runtime. Multi-port projects also have {port_2} through {port_10}.

Hooks do not support the full env token set. Tokens like {router_domain}, {resolve:...}, and {database} are not interpolated in hook commands. If your hook needs those values, read them from the env file that GTL writes, or reference the environment variables directly since they're already set by the time your server runs.

When hooks don't run

Start hooks are scoped to fresh starts. Several common scenarios skip them:

Scenario Hooks run? Why
Fresh gtl start Yes No supervisor running. Full lifecycle.
Resume (gtl start with supervisor alive) No Supervisor is already running. GTL warns if --with was passed.
gtl restart No Restart syncs env and restarts the server process but doesn't re-provision.
gtl stop No post_stop post_stop runs when the supervisor process exits (Ctrl+C), not when the server is stopped remotely.
gtl start --await pre_start only pre_start runs normally, but the calling process exits after the server is healthy. post_stop does not run from this process.

Hooks are not a process manager

Hooks run before and after the server. They're for quick provisioning steps, not for managing long-running processes. Don't put bundle exec sidekiq & in a pre_start hook. If you need sidecars, put them in your commands.start (e.g. via Foreman, Overmind, or a Procfile) where the supervisor manages their lifecycle.

Good uses for hooks: registering a router alias, seeding a database, toggling a debug flag, warming a cache. Bad uses: starting Sidekiq, running a webpack watcher, launching Redis. If the process needs to stay alive while the server runs, it belongs in commands.start.

Edge cases

  • Renaming a hook in YAML. If you rename a hook after a server was started with the old name, post_stop for the old name will be silently skipped on exit because the config no longer contains that hook name. Clean up manually if the hook registered external state (like an alias).
  • Multiple auto hooks. Go's map iteration order is non-deterministic. If you have two auto hooks and their order matters, combine them into one hook with multiple commands.
  • Failed pre_start. The hooks state file is only written after all pre_start commands succeed. If one fails, no state is persisted and post_stop will not run for any hook (since the server never started).
  • gtl stop vs Ctrl+C. gtl stop sends a signal to the supervisor from a separate process. The post_stop hooks run in the original gtl start process after the supervisor exits. If you ran gtl start in a terminal that's still open, post_stop will fire. If that terminal was closed, the hooks won't run.

Configuration reference

Field Type Description
hooks.<name>.pre_start string or list Commands to run before the supervisor launches.
hooks.<name>.post_stop string or list Commands to run after the supervisor exits. Reverse order.
hooks.<name>.auto bool Run this hook on every fresh gtl start. Default: false.
hooks.pre_setup list Before commands.setup. Failure aborts setup.
hooks.post_setup list After commands.setup. Failure warns only.
hooks.pre_release list After confirmation, before resource teardown. Failure aborts release.
hooks.post_release list After resource teardown. Failure warns only.

See it in action