Workspaces and hooks
Where each task's files live, how long they last, and how to run your own scripts when a workspace is created, used or removed.
Every task gets its own workspace directory. Agents work only there, and the sandbox mounts only that directory. Hooks let you run shell scripts at four points in a workspace's life, for example to install dependencies or seed a database.
Where workspaces live#
A task's workspace is <workspace.root>/<workspace key>:
workspace.rootcomes fromWORKFLOW.md. The wizard writes~/HarnessWorkspaces/<project>. Without it, each project gets<temp>/symphony_workspaces/<project>.- The workspace key is the task identifier with anything outside
A–Z a–z 0–9 . _ -replaced by_. When the identifier had to change, a short hash is added so two tasks can never share a folder.
With Cloudflare sandboxes the files live in Cloudflare; the local folder is only a placeholder.
Lifecycle#
| Event | What happens |
|---|---|
| First dispatch | The workspace is created. With managed git, the project is cloned into it on the task branch. Then after_create runs. |
| Each attempt | before_run runs, then the agent, then after_run. The branch is saved after every attempt. |
| Retries | The same workspace is reused, so the agent sees its earlier work. |
| Terminal state (Done, Canceled) | The branch is saved, before_remove runs, and the workspace is removed. A workspace whose branch couldn't be saved is kept. |
| App or server restart | Workspaces of tasks already in a terminal state are removed at startup. |
The Workspaces tab lists a project's workspaces with their branch, whether it's saved to the repository, and whether an agent is using it. You can reveal a workspace in Finder, open a terminal or an editor there, or remove it (the branch is saved first; uncommitted changes are lost).
Hooks#
hooks:
after_create: |
npm ci
before_run: |
npx prisma migrate reset --force --skip-seed
after_run: |
rm -rf .next/cache
before_remove: |
echo "removing $SYMPHONY_ISSUE_IDENTIFIER"
timeout_ms: 120000| Hook | When | If it fails |
|---|---|---|
after_create | Once, right after the workspace is created | The new directory is removed and the attempt fails |
before_run | Before every attempt | The attempt fails (and is retried) |
after_run | After every attempt | Ignored (logged) |
before_remove | Before the workspace is removed | Ignored (logged) |
Each hook runs with bash -lc, in the workspace directory, with a timeout of hooks.timeout_ms (default 60 s). Hooks get the merged project and global environment, plus:
| Variable | Value |
|---|---|
SYMPHONY_WORKSPACE | The workspace path (/workspace inside a sandbox) |
SYMPHONY_WORKSPACE_KEY | The workspace key |
SYMPHONY_WORKFLOW_DIR | The folder containing WORKFLOW.md (host hooks only) |
SYMPHONY_HOOK | The hook's name |
SYMPHONY_ISSUE_ID, SYMPHONY_ISSUE_IDENTIFIER, SYMPHONY_ISSUE_TITLE, SYMPHONY_ISSUE_STATE | The task |
SYMPHONY_ISSUE_BRANCH | The task branch, symphony/<key> |
Agent and gateway credentials (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, OPENAI_API_KEY, HARNESS_GATEWAY_API_KEY, LITELLM_API_KEY) and push tokens never reach hooks.
Where hooks run#
| Project runs agents in | Hooks run |
|---|---|
Docker (default sandbox.hooks: sandbox) | In a disposable container from the agent image, with the same isolation. During an attempt they get the agent's network policy; otherwise no network at all. Only SYMPHONY_* variables, the names in docker.env and git identity variables are passed in. |
| Cloudflare | In the task's Cloudflare sandbox, in /workspace. |
Host, or sandbox.hooks: host | On your machine, as your user, in the workspace directory. |
With sandbox.hooks: host and Docker on, hooks run outside the sandbox in a directory the agent controls. Anything they run there (git especially, or project scripts like npm lifecycle hooks) reads agent-written files. The inspector shows a warning. Keep hooks in the sandbox unless you're sure.