The security contract Symphony ships today. Symphony runs a coding agent with meaningful privilege in a trusted environment — this document is the threat model it assumes and the boundaries it enforces.
Warning. Symphony is an engineering preview. Do not expose its dashboard to untrusted networks.
Symphony is assumed to run on a trusted operator machine with:
- A private repo clone the operator controls.
- A
LINEAR_API_KEYscoped to the minimum set of teams + projects. - The
claudeCLI authenticated to an Anthropic account. - A
127.0.0.1:4000dashboard bound to loopback, not exposed publicly.
In this environment the attack surface we care about is:
- Malicious issue content. An issue title/description/label could contain shell injections, path traversal, or prompt injections targeting the agent.
- Compromised tracker. A trust-boundary violation in Linear's API could make fetched issues malicious.
- Stream poisoning. The
claudeCLI's stream-json output is untyped JSON from an external process; a bug in parsing could crash the orchestrator.
Not in scope today:
- Multi-tenant isolation (one operator, one Symphony instance).
- Dashboard authentication (loopback-only).
- Transport encryption for local HTTP (loopback-only).
See design-docs/threat-model.md for a
longer-form discussion.
assertSafeIdentifier(issue.identifier) in
src/workspace/manager.ts enforces
/^[A-Za-z0-9_-]+$/ on any string that reaches mkdir, execFile, or
workspace paths. A UnsafeIdentifierError surfaces with a human-readable
message rather than letting a traversal through.
Any new filesystem or shell usage keyed on external input must call this helper (or add its own allow-list).
WorkspaceManager.runHook exports a fixed set of env vars to the hook
process:
ISSUE_ID, ISSUE_IDENTIFIER, ISSUE_TITLE, ISSUE_STATE, ISSUE_URL, ISSUE_LABELS
Hooks consume them via "$ISSUE_IDENTIFIER", never by string-formatting. This
is the explicit contract PROGRESS.md calls out:
"Reject path-traversing issue identifiers; lock the env-var contract."
LinearTracker.gql ships queries as static strings with variables passed
separately. There is no string concatenation into GraphQL, so Linear input
can't break the query.
toAgentTurn accepts unknown, discriminates
on type, and rejects anything unexpected. Unparseable JSON lines are
dropped, not thrown. The stderrBuffer is capped at 8 KiB so a runaway
claude child can't OOM the parent.
The rendered prompt is stored in turns.rendered_prompt. It's derived from
the issue + attempt; it never includes process.env or other secrets. Prompt
templates MUST NOT {% include %} secret-bearing files. (The liquidjs
engine is configured without filesystem access by default.)
src/api/server.ts is served on :4000 with no auth.
src/cli.ts defaults --bind to 127.0.0.1 for both run
and replay, so the dashboard is reachable only from loopback unless the
operator explicitly opts out (e.g. --bind 0.0.0.0 behind a tunnel they
control). Putting the dashboard behind an exposed proxy without auth is out
of scope. Running this on a shared host without a tunnel is unsupported.
The orchestrator only talks to:
- Linear via
fetchImpl(throughLinearTracker). - The local
claudeCLI viaspawn.
It does not make arbitrary HTTP calls. New network capability requires a
design note in design-docs/ and a gate in the layer map.
LINEAR_API_KEY. Read fromprocess.envincli.ts. Never logged. Never written to the DB. Not echoed on startup.CLAUDE_CLI. Optional override for the claude binary path; no secret content.- Any hook-script secrets. Hooks inherit the parent
process.envminus explicit overrides. If a hook needs a credential (e.g.GITHUB_TOKEN), the operator injects it before launching Symphony; the harness does not broker or store it.
The CLI's prune subcommand deletes runs + JSONL files for runs older than
its argument. The default is 30d and it fails closed on bad duration strings.
Prune is idempotent but destructive; prefer it as a cron job, not an
automation hook.
git worktree remove --force in the before_remove hook is the other
destructive operation. It is scoped to the worktree the orchestrator just
created and keyed off the safe-identifier check above.
See ../NOTICE for the Apache-2.0 contact paths. Open a Github
issue only for non-sensitive reports; for sensitive disclosures mail the
maintainer directly.