Skip to main content
The hooks configuration section allows you to run custom shell scripts at key points in the workspace lifecycle.

Configuration

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_remove: |
    cd elixir && mise exec -- mix workspace.before_remove
  timeout_ms: 60000

Fields

after_create
string
Shell script executed immediately after a new workspace directory is created.Runs only once when the workspace is first created for an issue. On subsequent runs (workspace reuse), this hook does not execute.Execution context:
  • Working directory: Workspace path
  • Shell: sh -lc <script> (POSIX-compatible)
  • Timeout: hooks.timeout_ms
Failure handling:
  • Hook failure aborts workspace creation
  • The agent run fails and schedules a retry
  • Workspace directory may be removed on creation failure
Common use cases:
  • Clone repository
  • Install dependencies
  • Set up environment (mise, asdf, etc.)
  • Initialize configuration files
before_run
string
Shell script executed before each agent run attempt.Runs every time an agent session starts, including:
  • First run
  • Retry attempts
  • Continuation runs after hitting max_turns
Execution context:
  • Working directory: Workspace path
  • Shell: sh -lc <script>
  • Timeout: hooks.timeout_ms
Failure handling:
  • Hook failure aborts the current run attempt
  • The orchestrator schedules a retry with exponential backoff
Common use cases:
  • Sync latest code from remote (git pull)
  • Refresh dependencies
  • Update configuration
  • Pre-run validation
after_run
string
Shell script executed after each agent run attempt completes.Runs every time an agent session ends, regardless of outcome:
  • Success
  • Failure
  • Timeout
  • Cancellation/stall
Execution context:
  • Working directory: Workspace path (if it exists)
  • Shell: sh -lc <script>
  • Timeout: hooks.timeout_ms
Failure handling:
  • Hook failure is logged and ignored
  • The agent run’s original outcome is preserved
  • Cleanup/reconciliation continues normally
Common use cases:
  • Upload logs/artifacts
  • Post-run metrics collection
  • Cleanup temporary files
  • Send notifications
before_remove
string
Shell script executed before a workspace directory is deleted.Runs when:
  • Issue transitions to a terminal state (orchestrator cleanup)
  • Service startup cleanup for terminal issues
  • Manual workspace removal
Execution context:
  • Working directory: Workspace path (only if directory exists)
  • Shell: sh -lc <script>
  • Timeout: hooks.timeout_ms
Failure handling:
  • Hook failure is logged and ignored
  • Workspace removal proceeds even if hook fails
Common use cases:
  • Archive workspace state
  • Upload final artifacts
  • Clean up external resources
  • Send completion notifications
timeout_ms
integer
default:60000
Maximum execution time for all hooks in milliseconds (1 minute default).If a hook exceeds this timeout:
  • The process is killed (Task.shutdown(task, :brutal_kill))
  • The hook is treated as failed
  • Failure handling depends on the hook type (see above)
Dynamic: Changes apply to future hook executions without restart.Non-positive values are treated as invalid and fall back to the default.

Execution Context

All hooks execute in a task with:
System.cmd("sh", ["-lc", command], cd: workspace, stderr_to_stdout: true)
  • Shell: sh -lc (POSIX login shell)
  • Working directory: Workspace path
  • Output: stderr redirected to stdout
  • Timeout: hooks.timeout_ms

Environment Variables

Hooks inherit the environment from the Symphony process, including:
  • HOME, USER, PATH
  • Any custom variables set before launching Symphony
  • Shell profile/rc files are loaded (-l flag)

Exit Codes

Hook success/failure is determined by exit code:
  • 0: Success
  • Non-zero: Failure (logged with output)

Hook Lifecycle Example

For issue ABC-123 across multiple runs:

First Run (New Workspace)

1. Orchestrator dispatches ABC-123
2. Workspace.create_for_issue(ABC-123)
   - Directory created: ~/workspaces/ABC-123/
   - Hook: after_create (runs: git clone, mise setup)
3. Workspace.run_before_run_hook(ABC-123)
   - Hook: before_run (runs: git pull, refresh deps)
4. AgentRunner.run(ABC-123)
   - Codex session executes
5. Workspace.run_after_run_hook(ABC-123)
   - Hook: after_run (runs: upload logs)

Second Run (Workspace Reuse)

1. Orchestrator dispatches ABC-123 (retry/continuation)
2. Workspace.create_for_issue(ABC-123)
   - Directory exists: ~/workspaces/ABC-123/
   - Hook: after_create (SKIPPED - not a new workspace)
3. Workspace.run_before_run_hook(ABC-123)
   - Hook: before_run (runs: git pull, refresh deps)
4. AgentRunner.run(ABC-123)
   - Codex session executes
5. Workspace.run_after_run_hook(ABC-123)
   - Hook: after_run (runs: upload logs)

Terminal Cleanup

1. Issue ABC-123 transitions to "Done"
2. Orchestrator reconciliation detects terminal state
3. Workspace.remove(ABC-123)
   - Hook: before_remove (runs: archive workspace)
   - Directory removed: ~/workspaces/ABC-123/

Hook Output Handling

Hook output is logged for debugging:
def sanitize_hook_output_for_log(output, max_bytes \\ 2_048) do
  binary_output = IO.iodata_to_binary(output)

  case byte_size(binary_output) <= max_bytes do
    true -> binary_output
    false -> binary_part(binary_output, 0, max_bytes) <> "... (truncated)"
  end
end
Example log entry:
Workspace hook failed hook=after_create issue_id=abc123 issue_identifier=ABC-123 workspace=/path/to/workspace status=1 output="fatal: destination path '.' already exists...\n... (truncated)"

Common Patterns

Repository Clone and Setup

hooks:
  after_create: |
    git clone --depth 1 https://github.com/org/repo .
    git checkout main
    npm install
  before_run: |
    git fetch origin
    git reset --hard origin/main
    npm install

Python Environment

hooks:
  after_create: |
    python -m venv .venv
    source .venv/bin/activate
    pip install -r requirements.txt
  before_run: |
    source .venv/bin/activate
    pip install -r requirements.txt --upgrade

Tool Version Management (mise/asdf)

hooks:
  after_create: |
    mise trust
    mise install
    mise exec -- npm install
  before_run: |
    mise exec -- git pull
    mise exec -- npm install

Artifact Upload

hooks:
  after_run: |
    if [ -f run.log ]; then
      aws s3 cp run.log s3://bucket/logs/$(date +%s).log
    fi
  before_remove: |
    tar czf workspace.tar.gz .
    aws s3 cp workspace.tar.gz s3://bucket/archives/$ISSUE_ID.tar.gz

Conditional Logic

hooks:
  after_create: |
    if [ -f package.json ]; then
      npm install
    elif [ -f Gemfile ]; then
      bundle install
    elif [ -f requirements.txt ]; then
      pip install -r requirements.txt
    fi

Debugging Hooks

To test hook execution manually:
# Navigate to a workspace
cd ~/workspaces/ABC-123

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

# Check exit code
echo $?
To see hook logs from Symphony:
# Search logs for hook execution
grep "Running workspace hook" symphony.log
grep "Workspace hook failed" symphony.log
grep "Workspace hook timed out" symphony.log

Configuration Reloading

Hook configuration changes are applied dynamically:
  • New hook scripts take effect immediately
  • Timeout changes apply to future hook executions
  • Running hooks complete with their original configuration
No restart required.
  • workspace - Configure workspace root and layout
  • codex - Control agent execution environment
  • agent - Manage retry behavior when hooks fail

Build docs developers (and LLMs) love