Skip to main content
Workspace hooks are shell scripts that run at specific points in the workspace lifecycle. They enable custom setup, validation, and teardown logic for each issue workspace.

Hook Types

Symphony supports four lifecycle hooks, defined in WORKFLOW.md frontmatter:

after_create

Runs once when a workspace directory is newly created. Fatal if it fails.

before_run

Runs before each agent attempt. Fatal if it fails.

after_run

Runs after each agent attempt completes. Non-fatal.

before_remove

Runs before workspace deletion. Non-fatal.

Configuration

Define hooks in the hooks section of WORKFLOW.md:
WORKFLOW.md
---
hooks:
  after_create: |
    git clone --depth 1 https://github.com/openai/symphony .
    if command -v mise >/dev/null 2>&1; then
      cd elixir && mise trust && mise exec -- mix deps.get
    fi
  before_run: |
    git fetch origin
    git status
  after_run: |
    git add -A
    git status
  before_remove: |
    cd elixir && mise exec -- mix workspace.before_remove
  timeout_ms: 60000
---

Hook Configuration Fields

after_create
string
Multiline shell script that runs when a workspace is first created. Failure aborts workspace creation.Common uses:
  • Clone repository
  • Install dependencies
  • Generate config files
  • Bootstrap database
before_run
string
Multiline shell script that runs before each agent attempt. Failure aborts the current attempt.Common uses:
  • Pull latest changes
  • Sync dependencies
  • Validate environment
  • Update configuration
after_run
string
Multiline shell script that runs after each agent attempt. Failure is logged but ignored.Common uses:
  • Commit work in progress
  • Collect metrics
  • Archive logs
  • Update dashboards
before_remove
string
Multiline shell script that runs before workspace deletion. Failure is logged but ignored.Common uses:
  • Archive workspace artifacts
  • Clean up external resources
  • Notify monitoring systems
  • Save debugging snapshots
timeout_ms
integer
default:"60000"
Timeout in milliseconds for all hooks. Non-positive values fall back to default.Changes apply at runtime to future hook executions.

Execution Environment

Shell Context

Hooks execute in a local shell with these properties:
  • Shell: sh -lc <script> (POSIX-compatible, login shell)
  • Working directory: The workspace path
  • Standard streams: Combined stderr/stdout
  • Timeout: Configured via hooks.timeout_ms

Environment Variables

The workspace path and issue context are available implicitly:
# Current directory is the workspace
pwd  # /path/to/workspace/root/ISSUE-123

# Access environment if needed
echo $LINEAR_API_KEY  # From parent process

Implementation Details

From the Elixir implementation (lib/symphony_elixir/workspace.ex):
# Only runs when workspace is newly created
defp maybe_run_after_create_hook(workspace, issue_context, created?) do
  case created? do
    true ->
      case Config.workspace_hooks()[:after_create] do
        nil ->
          :ok

        command ->
          run_hook(command, workspace, issue_context, "after_create")
      end

    false ->
      :ok
  end
end

Use Cases

Repository Setup

Clone and prepare a repository when a workspace is created:
hooks:
  after_create: |
    git clone --depth 1 https://github.com/org/repo .
    npm install
    cp .env.example .env

Dependency Sync

Keep dependencies updated before each run:
hooks:
  before_run: |
    git fetch origin
    if [ -f package-lock.json ]; then
      npm ci
    fi

Work-in-Progress Commits

Automatically stage changes after each run:
hooks:
  after_run: |
    git add -A
    if ! git diff --cached --quiet; then
      git commit -m "WIP: auto-commit from Symphony"
    fi

Workspace Archival

Save debugging artifacts before cleanup:
hooks:
  before_remove: |
    tar czf /tmp/workspace-$(basename $PWD)-$(date +%s).tar.gz .
    echo "Archived workspace to /tmp/"

Database Bootstrap

Set up a test database for each workspace:
hooks:
  after_create: |
    # Clone repo
    git clone --depth 1 https://github.com/org/repo .
    
    # Create unique database
    export DB_NAME="test_$(basename $PWD)"
    createdb $DB_NAME
    
    # Run migrations
    DATABASE_URL="postgres://localhost/$DB_NAME" npm run migrate

Conditional Setup

Use shell conditionals for environment-specific setup:
hooks:
  after_create: |
    git clone --depth 1 https://github.com/org/repo .
    
    # Only install if tool is available
    if command -v mise >/dev/null 2>&1; then
      mise trust && mise install
    fi
    
    # Platform-specific setup
    if [ "$(uname)" = "Darwin" ]; then
      brew install special-tool
    fi

Error Handling

Fatal Hooks

after_create and before_run are fatal - failures abort the operation:
hooks:
  after_create: |
    git clone https://github.com/org/repo . || exit 1
    npm install || exit 1  # Abort if install fails
If after_create fails, the workspace directory may be removed and recreated on the next attempt.

Non-Fatal Hooks

after_run and before_remove are non-fatal - failures are logged but execution continues:
hooks:
  before_remove: |
    # Best-effort archival
    tar czf /tmp/archive.tar.gz . || true
    # Cleanup continues even if tar fails

Timeout Handling

Hooks that exceed timeout_ms are killed:
hooks:
  timeout_ms: 120000  # 2 minutes
  after_create: |
    # Long-running setup
    git clone https://github.com/org/large-repo .
    npm install  # May take time
Set timeout_ms based on your slowest hook. The default 60 seconds works for most cases.

Best Practices

1

Use idempotent operations

Hooks may be retried. Use idempotent commands:
# Good: idempotent
git clone https://github.com/org/repo . 2>/dev/null || git fetch origin

# Bad: fails on retry
git clone https://github.com/org/repo .
2

Handle missing tools gracefully

Check for tool availability before using:
if command -v mise >/dev/null 2>&1; then
  mise install
else
  echo "mise not available, skipping"
fi
3

Log important steps

Echo progress for debugging:
echo "Cloning repository..."
git clone https://github.com/org/repo .
echo "Installing dependencies..."
npm install
echo "Setup complete"
4

Set explicit exit codes

For fatal hooks, exit explicitly on failure:
npm install || exit 1
npm test || exit 1

Debugging

View Hook Logs

Hook execution is logged with structured context:
[info] Running workspace hook hook=after_create issue_id=abc123 issue_identifier=MT-649 workspace=/path/to/workspace
[warning] Workspace hook failed hook=after_create status=1 output="error: unable to clone"

Test Hooks Locally

Run hooks manually in a test workspace:
# Create test workspace
mkdir -p /tmp/test-workspace
cd /tmp/test-workspace

# Run hook command
sh -lc 'git clone --depth 1 https://github.com/org/repo .'

Common Issues

  • Increase timeout_ms for slow operations
  • Optimize the hook (shallow clones, skip optional steps)
  • Check for hung processes or network issues
  • Verify network access to clone source
  • Check file permissions on workspace root
  • Ensure required tools are available (git, npm, etc.)
  • Test the hook manually in isolation
  • Hook output is logged with the hook context
  • Check log files or console output
  • Output is truncated at 2048 bytes in logs

Reference

Hook Lifecycle

Configuration Schema

interface WorkspaceHooks {
  after_create?: string;   // Fatal on failure
  before_run?: string;     // Fatal on failure
  after_run?: string;      // Non-fatal
  before_remove?: string;  // Non-fatal
  timeout_ms?: number;     // Default: 60000
}

Build docs developers (and LLMs) love