Workspaces

Workspace Hydration

How Syntropic137 pre-clones repositories into workspace containers before the agent starts, with GitHub App token injection, namespace-safe directory naming, and automatic AGENTS.md/CLAUDE.md context injection.

Workspace hydration is the process of preparing a container with everything an agent needs before it starts. Repositories are cloned, credentials are installed, and agent context files are synthesized - all during the workspace setup phase, before the agent process runs.

The agent wakes up in a fully configured environment. It does not clone repos, manage credentials, or set up its own context. Hydration does that work up front.

Why Pre-Clone Instead of Agent-Clone?

When an agent clones its own repositories, several things go wrong:

  • Raw GitHub tokens leak into the agent's environment (a security violation under ADR-024)
  • Token injection time counts against the agent's context budget
  • The agent wastes turns on setup instead of actual work
  • Cloning errors disrupt the workflow mid-run

Workspace hydration solves all of these. Cloning happens during the setup phase using short-lived GitHub App installation tokens. Those tokens are cleared before the agent starts. The agent inherits cached git credentials and pre-populated repos - but never sees a raw token.

The repos Field

repos is a first-class field on both the workflow template and the execute API (ADR-058).

Workflow Template (default repos)

Set default repos when creating a workflow:

syn workflow create my-workflow \
  --repos https://github.com/acme/api-service \
  --repos https://github.com/acme/web-app

Or in a workflow YAML:

id: my-workflow-v1
name: My Workflow
repos:
  - https://github.com/acme/api-service
  - https://github.com/acme/web-app
phases:
  - id: analyze
    name: Analyze
    order: 1
    prompt_file: phases/analyze.md
    model: sonnet

Every execution of this workflow will pre-clone both repos into every phase, unless overridden at run time or opted out per phase with clone_repos.

Execute Override

Pass --repo at run time to override or extend the template's default repos:

# Override - use these repos instead of the template defaults
syn workflow run my-workflow-v1 \
  --repo https://github.com/acme/api-service \
  --repo https://github.com/acme/staging-fork

# Template has no repos - supply them per-run
syn workflow run my-workflow-v1 \
  --repo https://github.com/acme/feature-branch

The execute API accepts the same field:

curl -X POST /v1/workflows/{id}/execute \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Review the latest changes",
    "repos": [
      "https://github.com/acme/api-service",
      "https://github.com/acme/web-app"
    ]
  }'

Clone Directory Naming

Repositories are cloned under /workspace/repos/ as the bare repository name: the last segment of the URL with any .git suffix removed. The owner is not part of the path.

Repository URLClone Path
github.com/acme/api-service/workspace/repos/api-service/
github.com/acme/web-app/workspace/repos/web-app/
github.com/other-org/api-service/workspace/repos/api-service/

The first and third rows collide, and the collision is silent. The clone is guarded by [ -d <dest> ] || git clone ..., so when two repos with the same name from different orgs are configured on one workflow, the second clone is skipped without an error and the agent works on the first org's checkout believing it is the second's. Do not configure two same-named repos on one workflow.

$GH_REPO

Every phase has $GH_REPO set to the primary repo's owner/repo:

$GH_REPO=acme/api-service

It names the repository gh acts on. gh normally infers that from the git remotes of the surrounding working tree and fails before making any API call when there is no tree, so $GH_REPO is what makes gh pr create work from a phase that declared clone_repos: false. It is keyed on the same primary repo the prompt is told to work on, so the two cannot disagree.

There is no environment variable listing all of a workspace's repo paths. Iterate the directory instead of reconstructing the names:

# nullglob matters: /workspace/repos/ always exists and is EMPTY for a phase that
# set clone_repos: false. Without it the loop runs once, with $repo set to the
# literal string /workspace/repos/*/.
shopt -s nullglob
for repo in /workspace/repos/*/; do
  echo "Processing $repo"
done

AGENTS.md and CLAUDE.md Injection

When one or more repos are cloned, the orchestrator synthesizes /workspace/AGENTS.md and /workspace/CLAUDE.md at container start. Both files receive identical content: @-imports of the AGENTS.md and the CLAUDE.md of every cloned repo:

@/workspace/repos/api-service/AGENTS.md
@/workspace/repos/api-service/CLAUDE.md
@/workspace/repos/web-app/AGENTS.md
@/workspace/repos/web-app/CLAUDE.md

Each harness loads its own file automatically: Claude Code reads the synthesized CLAUDE.md, and Codex reads AGENTS.md. This means the agent inherits repo-specific conventions, architecture notes, and coding standards from every configured repository - with no manual setup.

If a cloned repo does not have an AGENTS.md or CLAUDE.md, it is skipped silently. If no repos are cloned - because none are configured, or because the phase set clone_repos: false - no synthesis happens and neither file is created.

Workspace Directory Structure

A hydrated workspace for a phase that clones looks like this:

/workspace/
  AGENTS.md              # Synthesized - imports each repo's AGENTS.md
  CLAUDE.md              # Synthesized - imports each repo's CLAUDE.md
  artifacts/
    input/               # Previous phase outputs (read-only)
    output/              # Current phase deliverables
  repos/
    api-service/         # Cloned from github.com/acme/api-service
    web-app/             # Cloned from github.com/acme/web-app
/opt/agentic/
  plugins/               # Pre-bundled plugins
  config/                # Runtime configuration
  version.json           # Image version manifest
  entrypoint.sh

Phases That Need Credentials But Not a Checkout

Credentials and a checkout are different needs. A phase that only opens a PR, comments on an issue or reads the API needs to be authenticated against the repos - it does not need them on disk, and cloning them costs time and disk for nothing.

Set clone_repos: false on that phase:

phases:
  - id: open-pr
    name: Open PR
    order: 2
    prompt_file: phases/open-pr.md
    clone_repos: false

It is a phase-level field and defaults to true. The repos stay configured on the workflow either way - do not drop them to skip the clone, because the repo list is what routes the GitHub App token to the right installation.

What the phase still gets:

  • A GitHub App installation token per repo, and the matching ~/.git-credentials entries
  • gh CLI configured, so gh pr create works
  • $GH_REPO

What it does not get:

  • Any repository checkout beneath /workspace/repos/. The directory itself is still there and empty: the workspace image creates it and the entrypoint creates it again, unconditionally. Code that inspects the workspace relies on that, so it can tell "this phase cloned nothing" apart from "the workspace did not answer" - do not write a check that reads a missing /workspace/repos/ as the signal for clone_repos: false, because it never is
  • The synthesized /workspace/AGENTS.md and /workspace/CLAUDE.md. Both are built from what was actually cloned, so for this phase there is nothing to import and neither file is written

The workspace prompt the agent receives is rendered to match: its tree shows repos/ present and empty, and it does not tell the agent to cd into a checkout that is not there. The directory is shown rather than omitted for the reason above - an agent told repos/ does not exist has been told the opposite of what the workspace guarantees.

GitHub App Token Lifecycle

Repo cloning uses GitHub App installation tokens, not personal access tokens. The lifecycle:

  1. Setup phase - Orchestrator generates a short-lived installation token per repo's org and injects it into the workspace via the sidecar proxy
  2. Clone - Each repo is cloned using the installation token; git credential helper caches the token for the repo URL
  3. Token revocation - Raw tokens are cleared from the environment before the agent phase starts (ADR-024)
  4. Agent phase - Agent uses cached git credentials for push/pull operations; it never handles the original token

The agent can push commits and open PRs using the cached credentials. The GitHub App's configured permissions determine what it can do.

Repos must be accessible to the GitHub App installation registered with Syntropic137. See GitHub Integration for setup. Repos outside the app's installation scope will fail to clone and the execution will error before the agent starts.

Unpushed Work at Phase End

A workspace is destroyed when its phase ends, and everything in it that is not on a remote goes with it. Before teardown, and while the git credentials are still live, Syntropic137 checks every repo cloned into the workspace for two things: commits that no remote has, and changes that were never committed at all.

If there are none - the normal case, and also the case for a phase that legitimately produces no commits at all - the phase completes as usual.

If there are, the work is saved and the phase fails. It is never reported as completed. For each affected repo, a commit is written whose tree is the working tree as the agent left it and whose parents are the local tips carrying the unpushed commits, and that commit is pushed to:

refs/syn/lost/<execution-id>/<phase-id>

The namespace is deliberately outside refs/heads and refs/tags. Nothing fetches it by default, no pull request shows it, and no reviewer is ever shown it. It exists to be recovered on purpose, by someone who was given the name.

Recovering Quarantined Work

The failure names the ref for every repo it saved, and prints the command beside it. From a clone of the affected repo:

git fetch origin refs/syn/lost/<execution-id>/<phase-id>
git log FETCH_HEAD
git switch -c recovered-work FETCH_HEAD

The unpushed commits are ancestors of FETCH_HEAD, so they read as the phase left them. Anything that was still uncommitted is folded into the quarantine commit itself, as its tree.

The quarantine push can itself fail - no network, a rejecting remote. When it does, the report says NOT RECOVERABLE for that repo and gives the push error instead of a ref, because that work really is gone. Read the summary line at the end of the report: it states whether all, some or none of the work can be fetched back, and it is counted from the refs that actually exist.

Work That Changed a GitHub Actions Workflow

The Syntropic137 GitHub App does not hold the workflows permission, and GitHub refuses the whole push when any commit in it changes a file under .github/workflows/. Without special handling, that one file would make every other change the phase made unrecoverable.

So when GitHub refuses the quarantine push for that reason - and only then - a second commit is pushed to the same ref:

  • Its tree is the working tree as the agent left it, except .github/workflows/, which is exactly as the last commit origin had (the commit named in the report).
  • Its only parent is that commit. History is flattened, because GitHub inspects every pushed commit and the originals carry the workflow change.
  • It adds a .syn-quarantine/ directory, or .syn-quarantine-2/ and so on if the phase already used that name. The directory holds:
    • workflows.patch, the dropped workflow changes;
    • unpushed.bundle, the original commits, when there were any.

The report names every dropped file, and the patch is also stored as a phase artifact. To get everything back, including the workflow changes:

git fetch origin refs/syn/lost/<execution-id>/<phase-id>
git switch -c recovered FETCH_HEAD
git apply .syn-quarantine/workflows.patch

To get the original commits instead of the flattened one:

git bundle unbundle .syn-quarantine/unpushed.bundle

That lists each tip with its commit id; git switch -c original <commit-id> checks one out.

Pushing the recovered workflow change still needs a credential that holds the workflows permission - yours, not the App's.

If the second push fails too, the report says NOT RECOVERABLE and names both refusals, but the workflow changes are still kept as the phase artifact.

Work committed inside a submodule of a cloned repo is detected - the superproject reports a modified gitlink, so the phase still fails rather than quietly succeeding - but the submodule's own objects are not quarantined, because they belong to a different remote. Submodule commits survive only if the phase pushed them itself.

Dashboard and Execution Detail

Configured repos are surfaced throughout the platform:

  • Execution detail - The repos field appears on WorkflowExecutionDetail, visible in the dashboard and via syn execution show <id>
  • Workflow detail - syn workflow show <id> displays the template's default repos
  • Execution list - Executions show their configured repos for filtering and audit

Syntropic137 Docs v0.33.1 · Last updated March 2026

On this page