A run rooted under ~ gets the operator's ~/.agents and ~/.claude skills in its system prompt #89

Closed
opened 2026-09-27 12:06:58 -04:00 by cmoriarty · 1 comment
Owner

Runs isolate opencode's HOME and XDG dirs (src/osf/agent/server.py, build_env / ISOLATION_ENV_KEYS) so the operator's config, MCP servers and skills never reach a run's system prompt (§21). That doesn't cover every path.

Measured (2026-09-27, opencode 1.18.21, osf.fakeinfer recording the upstream request)

With the run root, and so its work dir, under /Users/cmoriarty (for example ~/.osfm/rN/runs/r1/work), the system prompt was 14,228 characters instead of 9,707. The difference was a <skill> index listing /Users/cmoriarty/.agents/skills/* (caveman, caveman-commit, ...). The same run rooted under /private/tmp had none of those entries. That is about 1.5k tokens a turn, per step, on a laptop osfd whose root is under ~. Production roots runs under /opt/openspecflow, so it is not affected.

Cause (from the 1.18.21 bundle, Skill.discovery)

Unless OPENCODE_DISABLE_EXTERNAL_SKILLS is set, opencode scans for skills/**/SKILL.md in two places:

  1. <home>/.claude and <home>/.agents. The home comes from HOME, which is isolated, so this scan finds nothing.
  2. up({targets: [".claude", ".agents"], start: directory, stop: worktree}), a walk-up from the session directory to the project's worktree. For a directory that is not a git repo, opencode's worktree is /, so the walk goes to the filesystem root and picks up ~/.agents/skills and ~/.claude/skills. In a git checkout it stops at the repo root.

Reproduced with a probe skill planted in .agents/skills and .claude/skills above the run root. With the work dir not a git repo, both probe skills show up in GET /skill. With git init in the work dir, neither does.

Fix

  • build_env sets OPENCODE_DISABLE_EXTERNAL_SKILLS=1, which switches off both scans whether or not the work dir is a git repo.
  • That flag also drops the project's own .claude/skills and .agents/skills. For example, a dogfood run on openspec-flow sees its openspec-* skills today. So the per-run opencode.json adds them back explicitly with skills.paths: [".claude/skills", ".agents/skills"], which opencode resolves against the session directory. The run keeps the project's skills, and nothing above the work dir can leak in.
  • A needs_opencode live test plants skills above the run root and asserts that they do not reach GET /skill, while the project's own skills do.
Runs isolate opencode's HOME and XDG dirs (`src/osf/agent/server.py`, `build_env` / `ISOLATION_ENV_KEYS`) so the operator's config, MCP servers and skills never reach a run's system prompt (§21). That doesn't cover every path. ## Measured (2026-09-27, opencode 1.18.21, `osf.fakeinfer` recording the upstream request) With the run root, and so its work dir, under `/Users/cmoriarty` (for example `~/.osfm/rN/runs/r1/work`), the system prompt was **14,228 characters instead of 9,707**. The difference was a `<skill>` index listing `/Users/cmoriarty/.agents/skills/*` (caveman, caveman-commit, ...). The same run rooted under `/private/tmp` had none of those entries. That is about 1.5k tokens a turn, per step, on a laptop osfd whose root is under ~. Production roots runs under `/opt/openspecflow`, so it is not affected. ## Cause (from the 1.18.21 bundle, `Skill.discovery`) Unless `OPENCODE_DISABLE_EXTERNAL_SKILLS` is set, opencode scans for `skills/**/SKILL.md` in two places: 1. `<home>/.claude` and `<home>/.agents`. The home comes from `HOME`, which is isolated, so this scan finds nothing. 2. `up({targets: [".claude", ".agents"], start: directory, stop: worktree})`, a walk-up from the session directory to the project's worktree. For a directory that is **not a git repo**, opencode's worktree is `/`, so the walk goes to the filesystem root and picks up `~/.agents/skills` and `~/.claude/skills`. In a git checkout it stops at the repo root. Reproduced with a probe skill planted in `.agents/skills` and `.claude/skills` above the run root. With the work dir not a git repo, both probe skills show up in `GET /skill`. With `git init` in the work dir, neither does. ## Fix - `build_env` sets `OPENCODE_DISABLE_EXTERNAL_SKILLS=1`, which switches off both scans whether or not the work dir is a git repo. - That flag also drops the project's **own** `.claude/skills` and `.agents/skills`. For example, a dogfood run on openspec-flow sees its `openspec-*` skills today. So the per-run `opencode.json` adds them back explicitly with `skills.paths: [".claude/skills", ".agents/skills"]`, which opencode resolves against the session directory. The run keeps the project's skills, and nothing above the work dir can leak in. - A `needs_opencode` live test plants skills above the run root and asserts that they do not reach `GET /skill`, while the project's own skills do.
Author
Owner

Shipped on main in 665f6fe (fix), with the proposal in dcf78d0 and the archive in e0f3bec. The change was OpenSpec run-skills-stay-in-the-worktree, and it added a new spec, openspec/specs/run-isolation.

  • Env var: build_env sets OPENCODE_DISABLE_EXTERNAL_SKILLS=1, written after the caller's layers so they cannot undo it. This stops 1.18.21's walk-up for .claude/.agents skills, which reaches / when the work dir isn't a git repo.
  • Project skills kept: the per-run opencode.json sets skills.paths: [".claude/skills", ".agents/skills"], relative to the session directory, so the project's own skills still load. .opencode/skills is unaffected.
  • Tests: unit tests for both settings, and the live test test_skills_above_the_run_root_never_reach_the_run, parametrized over a git and a plain work dir. Without the fix, the plain-dir case lists both skills planted above the run root.
  • Measured on the wire with osf.fakeinfer and the run root under ~: the implement floor was 37,732 characters without the fix and 33,077 with it, 4,655 fewer, in line with the 4,521 in the report.

The stale live tests found along the way were fixed under #97.

Shipped on main in 665f6fe (fix), with the proposal in dcf78d0 and the archive in e0f3bec. The change was OpenSpec `run-skills-stay-in-the-worktree`, and it added a new spec, `openspec/specs/run-isolation`. - **Env var:** `build_env` sets `OPENCODE_DISABLE_EXTERNAL_SKILLS=1`, written after the caller's layers so they cannot undo it. This stops 1.18.21's walk-up for `.claude`/`.agents` skills, which reaches `/` when the work dir isn't a git repo. - **Project skills kept:** the per-run `opencode.json` sets `skills.paths: [".claude/skills", ".agents/skills"]`, relative to the session directory, so the project's own skills still load. `.opencode/skills` is unaffected. - **Tests:** unit tests for both settings, and the live test `test_skills_above_the_run_root_never_reach_the_run`, parametrized over a git and a plain work dir. Without the fix, the plain-dir case lists both skills planted above the run root. - **Measured on the wire** with `osf.fakeinfer` and the run root under `~`: the implement floor was 37,732 characters without the fix and 33,077 with it, 4,655 fewer, in line with the 4,521 in the report. The stale live tests found along the way were fixed under #97.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cmoriarty/braid#89
No description provided.