How to write runtime hooks

This guide shows how to write each of the five runtime hooks an SDK can ship, with a synthesized SDK that exercises the contract differences: which user the hook runs as, which working directory it starts in, and which environment variables it can rely on.

Workshop runs each hook as a bash script with errexit and pipefail set, so any non-zero exit aborts the hook. Where it differs is the privilege, working directory, and extra environment.

Prerequisites

You need a working SDKcraft installation and a workshop you can launch and refresh on a host with Workshop installed. The examples use a synthesized SDK named dotfiles-sdk. If you don’t have an SDK yet, Craft SDKs with SDKcraft walks through scaffolding one with sdkcraft init.

Lay out the hooks directory

SDKcraft picks up any executable file in the SDK’s hooks/ directory whose name matches one of the five hook names. Hooks are not listed in sdkcraft.yaml; SDKcraft enumerates them automatically when packing.

A complete hook tree looks like this:

$ ls hooks/

  check-health
  restore-state
  save-state
  setup-base
  setup-project

Each file is a bash script; mark it executable (sdkcraft init already does this for the scaffolded ones).

Write setup-base

setup-base runs as root, before the project directory is mounted and before plugs and slots are connected. It runs when the SDK is installed and again when its revision changes; a refresh that leaves the SDK intact skips it. The working directory is the SDK’s own hooks/ directory. Use it for system-wide preparation that other SDKs in the workshop may want to rely on:

hooks/setup-base
cat <<PROFILE >/etc/profile.d/dotfiles.sh
export DOTFILES_SDK="$SDK"
PROFILE

$SDK always points at the SDK installation directory in the workshop, so hooks can reference files the SDK shipped without hardcoding a path.

Write setup-project

setup-project runs as the workshop user, not root, with the working directory set to /project/. It also has $HOME, $XDG_RUNTIME_DIR, and $DBUS_SESSION_BUS_ADDRESS available, so it can touch the user’s home tree and talk to user-session services.

Use it for per-project initialization:

hooks/setup-project
id -u >"$HOME/.dotfiles-uid"
install -m 0644 -t "$HOME" "$SDK/skel/.bash_aliases"

The hook runs after auto-connect has finished, so any mounts the SDK plugged into are visible at this point, as well as the project directory itself, and the home directory is writable by the workshop user.

Write check-health

check-health runs as root from the SDK’s hooks/ directory. It is meant to be quick: each attempt has five seconds to report its result through workshopctl set-health and exit. A hook that runs past that window, or exits without reporting a status, moves the SDK’s health to error.

Call workshopctl set-health okay when everything is in order; otherwise, set error with a short message:

hooks/check-health
if ! sudo -u workshop --login bash -c 'test -f "$HOME/.dotfiles-uid"'; then
  workshopctl set-health error "setup-project marker missing"
  exit 0
fi
workshopctl set-health okay

Because check-health runs as root, use sudo -u workshop whenever the check needs the workshop user’s shell, environment, or file ownership.

Persist state with save-state and restore-state

When a workshop refreshes an SDK to a new revision, anything that lives outside a connected plug disappears unless the SDK explicitly preserves it. save-state and restore-state solve that case. Both hooks run as root from the SDK’s hooks/ directory and have $SDK_STATE_DIR available, pointing at a directory that survives the refresh.

save-state runs from the old SDK revision before the swap. Write whatever needs to outlive the revision into $SDK_STATE_DIR:

hooks/save-state
if [ -f /home/workshop/.dotfiles-uid ]; then
  cp /home/workshop/.dotfiles-uid "$SDK_STATE_DIR/"
fi

restore-state runs from the new SDK revision after the swap and after setup-project has finished for every SDK in the workshop:

hooks/restore-state
if [ -f "$SDK_STATE_DIR/.dotfiles-uid" ]; then
  install -m 0644 -o 1000 -g 1000 \
    "$SDK_STATE_DIR/.dotfiles-uid" \
    /home/workshop/.dotfiles-restored-uid
fi

Keep the hook idempotent and tolerant of missing input, since the new revision may be installed onto a workshop that was originally launched without save-state.

Verify the hooks

Build and install the SDK into a workshop with sdkcraft try:

$ sdkcraft try

SDKcraft lints every hook with ShellCheck while packing, so a shell error in a hook fails the build at this step.

List the SDK in a workshop definition with the try- prefix and launch the workshop:

.workshop/dev.yaml
name: dev
base: ubuntu@22.04
sdks:
  - name: try-dotfiles-sdk
$ workshop launch dev

At this point setup-base, setup-project, and check-health have all run. Confirm:

$ workshop exec dev -- cat /etc/profile.d/dotfiles.sh

  export DOTFILES_SDK="/var/lib/workshop/sdk/dotfiles-sdk"

$ workshop exec dev -- cat /home/workshop/.dotfiles-uid

  1000

$ workshop info dev

  name:     dev
  base:     ubuntu@22.04
  project:  /home/user/workshop/dev
  status:   ready

save-state and restore-state only run when workshop refresh has work to do: a new SDK revision to swap in, an added or removed SDK, or a change to the workshop definition. A bare workshop refresh dev against an unchanged workshop is a no-op and skips every hook.

To exercise the state hooks, edit the workshop definition so the refresh has something to apply, for example by adding a mount, and run workshop refresh for the workshop. After the refresh, .dotfiles-restored-uid exists in the workshop user’s home, confirming that save-state wrote into $SDK_STATE_DIR and restore-state read it back.

See also

Explanation:

Reference:

Tutorial: