Hooks are shell scripts that run at specific points in a workspace's lifecycle — when it's created, before and after the agent runs, and before deletion. They handle the gap between "empty directory exists" and "workspace is ready for an agent to write code in."
Prerequisites¶
- A working Sortie setup (quick start)
- A git repository the orchestrator host can clone (SSH key or token access configured)
Understand when each hook fires¶
Four hooks cover the workspace lifecycle. Each runs with the workspace directory as its working directory:
| Hook | Fires when | Failure effect |
|---|---|---|
after_create |
Workspace directory is created for the first time | Fatal — aborts workspace creation |
before_run |
Before each agent attempt, including retries | Fatal — aborts the current attempt |
after_run |
After each agent attempt (success or failure) | Logged, ignored |
before_remove |
Before workspace deletion | Logged, ignored |
A typical issue lifecycle looks like this:
Issue dispatched
│
├─ Directory created (first time)
│ └─ after_create ← clone repo, install deps
│
├─ before_run ← create branch, pull latest
│ └─ Agent runs...
│ └─ after_run ← commit changes, run formatter
│
├─ (retry — before_run → agent → after_run again)
│
└─ Issue reaches terminal state
├─ before_remove ← push branch, clean up remote
└─ Directory deleted
Notice that after_create runs once. before_run and after_run run on every attempt — first run, continuations, and retries.
Clone a repository on workspace creation¶
The most common after_create hook clones your project into the fresh workspace directory:
hooks:
after_create: |
git clone --depth 1 git@github.com:acme/backend.git .
The trailing . clones into the current directory (which is the workspace). --depth 1 keeps clones fast by fetching only the latest commit.
If the project needs dependencies after cloning, chain the commands:
hooks:
after_create: |
git clone --depth 1 git@github.com:acme/backend.git .
go mod download
Because after_create failure is fatal, a failed clone prevents the agent from running in a broken workspace. Sortie retries with backoff — the next attempt creates the workspace from scratch.
Create a branch before each run¶
before_run fires before every agent attempt. Use it to set up a clean branch so each attempt starts from the latest upstream code:
hooks:
before_run: |
git fetch origin main
git checkout -B "sortie/${SORTIE_ISSUE_IDENTIFIER}" origin/main
git checkout -B creates or resets the branch. On the first run, it creates sortie/PROJ-42. On a retry, it resets that branch to the latest main, discarding the failed attempt's changes. This gives each attempt a clean starting point.
If your workflow needs to preserve changes across retries, skip the reset and merge instead:
hooks:
before_run: |
git fetch origin main
if [ "$SORTIE_ATTEMPT" -gt 0 ]; then
git checkout "sortie/${SORTIE_ISSUE_IDENTIFIER}"
git merge origin/main --no-edit || git merge --abort
else
git checkout -B "sortie/${SORTIE_ISSUE_IDENTIFIER}" origin/main
fi
Commit and format after each run¶
after_run fires after every agent attempt regardless of outcome. Use it to preserve the agent's work:
hooks:
after_run: |
make fmt 2>/dev/null || true
git add -A
git diff --cached --quiet || git commit -m "sortie(${SORTIE_ISSUE_IDENTIFIER}): automated changes"
The || true after make fmt prevents a formatter failure from producing noisy logs — after_run failures are ignored anyway, but clean logs are worth the guard.
git diff --cached --quiet checks whether there's anything to commit. If the agent made no changes (or the run failed before writing files), the hook exits cleanly without creating an empty commit.
Clean up on workspace removal¶
before_remove fires when Sortie deletes a workspace directory — typically after the issue reaches a terminal state. Use it to clean up remote resources:
hooks:
before_remove: |
git push origin --delete "sortie/${SORTIE_ISSUE_IDENTIFIER}" 2>/dev/null || true
The 2>/dev/null || true suppresses errors when the branch doesn't exist remotely (for example, if the run never pushed). before_remove failures are logged and ignored — cleanup still proceeds.
Use hook environment variables¶
Every hook receives four variables from the orchestrator:
| Variable | Example | Description |
|---|---|---|
SORTIE_ISSUE_ID |
10042 |
Tracker-internal issue ID |
SORTIE_ISSUE_IDENTIFIER |
PROJ-42 |
Human-readable ticket key |
SORTIE_WORKSPACE |
/tmp/sortie_workspaces/PROJ-42 |
Absolute workspace path |
SORTIE_ATTEMPT |
0 |
Current attempt number (0 = first try) |
Hooks run in a restricted environment. Only PATH, HOME, SHELL, TMPDIR, and variables prefixed with SORTIE_ are available. Secrets like JIRA_API_TOKEN are stripped. If a hook needs additional credentials, expose them under a SORTIE_ prefix in the Sortie process environment (for example, SORTIE_DEPLOY_KEY) or load them from a file inside the script.
Set a timeout¶
All hooks share a single timeout controlled by hooks.timeout_ms. The default is 60 seconds. For repositories that take longer to clone or have heavy dependency installs, increase it:
hooks:
after_create: |
git clone git@github.com:acme/monorepo.git .
npm ci
timeout_ms: 180000
A timed-out hook is treated the same as a failure — fatal for after_create and before_run, ignored for after_run and before_remove.
Put it all together¶
Here is a complete hooks configuration for a Go project tracked in Jira:
hooks:
after_create: |
git clone --depth 1 $SORTIE_REPO_URL .
go mod download
before_run: |
git fetch origin main
git checkout -B "sortie/${SORTIE_ISSUE_IDENTIFIER}" origin/main
after_run: |
make fmt 2>/dev/null || true
git add -A
git diff --cached --quiet || git commit -m "sortie(${SORTIE_ISSUE_IDENTIFIER}): automated changes"
before_remove: |
git push origin --delete "sortie/${SORTIE_ISSUE_IDENTIFIER}" 2>/dev/null || true
timeout_ms: 120000
Verify hooks are running¶
Start Sortie and watch the logs for hook activity:
sortie ./WORKFLOW.md
On the first dispatch, you should see workspace creation followed by the hook:
level=INFO msg="workspace prepared" issue_id=42 issue_identifier=PROJ-42 workspace=/tmp/sortie_workspaces/PROJ-42
If a hook fails, the logs show the error and output:
level=ERROR msg="after_create hook failed" issue_id=42 issue_identifier=PROJ-42 error="exit status 128" output="fatal: repository 'git@...' not found"
Troubleshooting¶
"Permission denied (publickey)" during clone.
The SSH agent isn't available inside the hook. Verify that SSH_AUTH_SOCK is set in the Sortie process environment — it's on the allowlist and will pass through. Run ssh -T git@github.com as the same user that runs Sortie to confirm key access.
Hook works locally but fails under Sortie.
Hooks run in a restricted environment. Commands that depend on ~/.bashrc (like nvm or pyenv) won't find their shims. Wrap them with bash -lc '...' to source the login profile:
hooks:
after_create: |
git clone --depth 1 git@github.com:acme/frontend.git .
bash -lc 'nvm use 20 && npm ci'
Timeout on large repositories.
Increase hooks.timeout_ms. Use git clone --depth 1 or git clone --filter=blob:none for faster clones.
For the full hooks schema, see the WORKFLOW.md reference. For hooks in SSH-distributed setups, see scaling agents with SSH.