Summary
Extend the task field in api.ScionConfig (and the positional arg to scion start) to support a file:// URI, resolved on the host before container launch, matching the existing system_prompt: file:// pattern already in the inline-config system at cmd/common.go:132-141. The implementation is mostly infrastructure-reuse: <agentDir>/prompt.md is already written per-agent at pkg/agent/provision.go:284-288, but it's currently not bind-mounted — the content is extracted back out and passed inline as a CLI arg. This issue proposes mounting the file and passing a shell-safe container-side path to the harness.
Related: #162 (independent bug-fix for shell-escape correctness in the current inline path). This issue is additive — a cleaner path for structured prompts — rather than a fix for the escaping bug. Both stand on their own.
Motivation
Task prompts are frequently authored as markdown — code fences, backticks, headings, checklists, price/ID references ($), file globs. Passing markdown through the current tmux command string even after #162 still commits every consumer to perfect shell-quoting of every character Scion might ever consider special. File indirection sidesteps the problem entirely: the content never travels through a shell.
The file:// URI approach is preferred over a new --prompt-file flag because:
- Precedent already exists.
cmd/common.go:132-141 resolves system_prompt: file://prompt.md host-side; the same mechanism applied to task keeps the CLI and inline-config surfaces symmetric.
- No new CLI flag.
scion start <agent> "file:///path/to/task.md" reuses the existing positional arg.
- Composable with inline-config. A grove-level
scion-agent.yaml can declare task: file://prompt.md just like it already can for system_prompt.
Current state
pkg/agent/provision.go:284-288 writes the task string to <agentDir>/prompt.md on the host at provision time. pkg/agent/run.go:147-163 reads it back if the CLI task is empty, or overwrites it with the current task if provided. But the file itself is never mounted into the container — each harness receives the task as an inline CLI arg:
pkg/harness/claude_code.go:83-85 — bare positional arg
pkg/harness/gemini_cli.go:82-84 — --prompt-interactive <task>
pkg/harness/codex.go:68-70 — positional arg
pkg/harness/opencode.go:67-70 — --prompt <task>
The mount primitives needed to change this already exist:
pkg/runtime/common.go:607-682 — writeFileSecrets() — stages host file and emits -v host:container:ro mount spec
pkg/runtime/common.go:560-602 — applyResolvedAuth() — api.FileMapping{SourcePath, ContainerPath} → mount arg
registerMount flow in buildCommonRunArgs — the spot a prompt mount would hook into
Proposed design
Resolution:
cmd/common.go — when the task string (CLI positional arg or scion-agent.yaml task: field) starts with file://, resolve the path on the host (tilde-expand; relative paths resolved against grove root).
pkg/agent/run.go — copy/symlink the resolved file into <agentDir>/prompt.md if not already there (i.e., unify with the existing prompt.md path rather than introducing a second location). Fail loudly if the source file doesn't exist.
pkg/runtime/common.go / buildCommonRunArgs — append a -v <agentDir>/prompt.md:/home/scion/prompt.md:ro mount to the run args (container path can be any stable location; /home/scion/prompt.md matches the "home-adjacent" convention used by other mounts).
- Each harness's
GetCommand — when the task was file-resolved, pass the container-side path as the task arg instead of the file contents. Concretely: the harnesses use whatever flag/positional they already use today, but the value is /home/scion/prompt.md (shell-safe literal) rather than the file's contents.
Step 4 is where the design question lives: do harnesses accept a file path as their task arg?
- Claude Code: positional arg is a prompt string, not a file path. The agent would need to be told "the prompt is in this file" explicitly. Simplest path: pass a short literal prompt like
"Read /home/scion/prompt.md and execute the task described there." This is what makes the whole scheme shell-safe — the literal prompt has no metacharacters.
- Gemini CLI:
--prompt-interactive takes a string; same treatment.
- Codex / OpenCode: same.
The short literal prompt is boilerplate per-harness but deterministic and auditable. None of the harnesses have a native --prompt-file flag today (confirmed against current pkg/harness/*.go), so this Scion-level indirection is the right layer.
Alternative considered: stdin-piping the prompt into the harness. Rejected because it requires restructuring the tmux session launch (interactive harnesses read stdin from the tmux PTY, not from a pipe) and because it doesn't compose with the sciontool hook lifecycle.
Acceptance
task: in scion-agent.yaml and the positional arg to scion start accept file:// URIs with the same resolution semantics as system_prompt: file://.
- Resolved file lands at a stable container-side path (proposed:
/home/scion/prompt.md) via the existing registerMount / api.FileMapping plumbing.
- Harness
GetCommand implementations pass a short literal prompt instructing the harness to read the container-side file, for all supported harnesses (Claude Code, Gemini CLI, Codex, OpenCode).
- Integration test:
scion start with a task: file://... pointing at a markdown file containing backticks, $, globs, parens, and multi-line content lands the exact file contents at the container-side mount point, byte-for-byte. No shell interpretation.
- Missing source file fails loudly at agent start (
task: file://... not found at <resolved-path>), exit non-zero.
- Existing inline task strings (no
file:// prefix) work unchanged — this is opt-in.
Out of scope
- Changing the default behavior (positional arg without
file:// keeps its current semantics).
- Per-harness native
--prompt-file flags (not currently available in the target harnesses; Scion-level indirection is sufficient).
- Templated paths in the URI (
file://{{ grove }}/task.md) — ship with literal paths first.
- Hot-reload of the task file into a running agent.
Context
Branch main, commit 22dfe46a. No prior issue or PR addresses prompt-file indirection. The system_prompt: file:// precedent at cmd/common.go:132-141 is the closest idiom in the codebase. #162 is independent — even without file:// support, the escaping bug should be fixed on the inline path.
Summary
Extend the
taskfield inapi.ScionConfig(and the positional arg toscion start) to support afile://URI, resolved on the host before container launch, matching the existingsystem_prompt: file://pattern already in the inline-config system atcmd/common.go:132-141. The implementation is mostly infrastructure-reuse:<agentDir>/prompt.mdis already written per-agent atpkg/agent/provision.go:284-288, but it's currently not bind-mounted — the content is extracted back out and passed inline as a CLI arg. This issue proposes mounting the file and passing a shell-safe container-side path to the harness.Related: #162 (independent bug-fix for shell-escape correctness in the current inline path). This issue is additive — a cleaner path for structured prompts — rather than a fix for the escaping bug. Both stand on their own.
Motivation
Task prompts are frequently authored as markdown — code fences, backticks, headings, checklists, price/ID references (
$), file globs. Passing markdown through the current tmux command string even after #162 still commits every consumer to perfect shell-quoting of every character Scion might ever consider special. File indirection sidesteps the problem entirely: the content never travels through a shell.The
file://URI approach is preferred over a new--prompt-fileflag because:cmd/common.go:132-141resolvessystem_prompt: file://prompt.mdhost-side; the same mechanism applied totaskkeeps the CLI and inline-config surfaces symmetric.scion start <agent> "file:///path/to/task.md"reuses the existing positional arg.scion-agent.yamlcan declaretask: file://prompt.mdjust like it already can forsystem_prompt.Current state
pkg/agent/provision.go:284-288writes the task string to<agentDir>/prompt.mdon the host at provision time.pkg/agent/run.go:147-163reads it back if the CLI task is empty, or overwrites it with the current task if provided. But the file itself is never mounted into the container — each harness receives the task as an inline CLI arg:pkg/harness/claude_code.go:83-85— bare positional argpkg/harness/gemini_cli.go:82-84—--prompt-interactive <task>pkg/harness/codex.go:68-70— positional argpkg/harness/opencode.go:67-70—--prompt <task>The mount primitives needed to change this already exist:
pkg/runtime/common.go:607-682—writeFileSecrets()— stages host file and emits-v host:container:romount specpkg/runtime/common.go:560-602—applyResolvedAuth()—api.FileMapping{SourcePath, ContainerPath}→ mount argregisterMountflow inbuildCommonRunArgs— the spot a prompt mount would hook intoProposed design
Resolution:
cmd/common.go— when the task string (CLI positional arg orscion-agent.yamltask:field) starts withfile://, resolve the path on the host (tilde-expand; relative paths resolved against grove root).pkg/agent/run.go— copy/symlink the resolved file into<agentDir>/prompt.mdif not already there (i.e., unify with the existingprompt.mdpath rather than introducing a second location). Fail loudly if the source file doesn't exist.pkg/runtime/common.go/buildCommonRunArgs— append a-v <agentDir>/prompt.md:/home/scion/prompt.md:romount to the run args (container path can be any stable location;/home/scion/prompt.mdmatches the "home-adjacent" convention used by other mounts).GetCommand— when the task was file-resolved, pass the container-side path as the task arg instead of the file contents. Concretely: the harnesses use whatever flag/positional they already use today, but the value is/home/scion/prompt.md(shell-safe literal) rather than the file's contents.Step 4 is where the design question lives: do harnesses accept a file path as their task arg?
"Read /home/scion/prompt.md and execute the task described there."This is what makes the whole scheme shell-safe — the literal prompt has no metacharacters.--prompt-interactivetakes a string; same treatment.The short literal prompt is boilerplate per-harness but deterministic and auditable. None of the harnesses have a native
--prompt-fileflag today (confirmed against currentpkg/harness/*.go), so this Scion-level indirection is the right layer.Alternative considered: stdin-piping the prompt into the harness. Rejected because it requires restructuring the tmux session launch (interactive harnesses read stdin from the tmux PTY, not from a pipe) and because it doesn't compose with the
sciontool hooklifecycle.Acceptance
task:inscion-agent.yamland the positional arg toscion startacceptfile://URIs with the same resolution semantics assystem_prompt: file://./home/scion/prompt.md) via the existingregisterMount/api.FileMappingplumbing.GetCommandimplementations pass a short literal prompt instructing the harness to read the container-side file, for all supported harnesses (Claude Code, Gemini CLI, Codex, OpenCode).scion startwith atask: file://...pointing at a markdown file containing backticks,$, globs, parens, and multi-line content lands the exact file contents at the container-side mount point, byte-for-byte. No shell interpretation.task: file://... not found at <resolved-path>), exit non-zero.file://prefix) work unchanged — this is opt-in.Out of scope
file://keeps its current semantics).--prompt-fileflags (not currently available in the target harnesses; Scion-level indirection is sufficient).file://{{ grove }}/task.md) — ship with literal paths first.Context
Branch
main, commit22dfe46a. No prior issue or PR addresses prompt-file indirection. Thesystem_prompt: file://precedent atcmd/common.go:132-141is the closest idiom in the codebase. #162 is independent — even withoutfile://support, the escaping bug should be fixed on the inline path.