Skip to content

fix(skills): support opencode backend (unknown backend "opencode")#147

Merged
Desperado merged 3 commits into
mainfrom
strazhnyk/fix-opencode-skills-backend
Jul 13, 2026
Merged

fix(skills): support opencode backend (unknown backend "opencode")#147
Desperado merged 3 commits into
mainfrom
strazhnyk/fix-opencode-skills-backend

Conversation

@Desperado

Copy link
Copy Markdown
Contributor

Problem

Activating the opencode backend (shipped in 1.20.8 / #141) crashed skill install with:

✗ Skill install failed: skills: unknown backend "opencode"

The /orch picker passes the selected backend into the generic setup.InstallSkillsReport(result.Backend) call (repl.go:537), but the skills materializer only knew about cc and codex — the opencode backend was added without wiring its skill format.

Root cause

internal/skills/materialize.go SkillsDir/renderSkillMD had no opencode case, so Materialize("opencode", home) hit the unknown backend error path. opencode genuinely supports skills (docs: ~/.config/opencode/skills/<name>/SKILL.md), so the fix is to make it a first-class backend rather than silently skip.

Fix

  • materialize.go: add BackendOpenCode → writes ~/.config/opencode/skills/<name>/SKILL.md, frontmatter name+description only (opencode ignores allowed-tools; no openai.yaml sibling).
  • main.go: sync skills on every opencode activation (idempotent), mirroring the cc/codex startup sync. opencode has no global-install prompt (consent.go skips it; qmax manages its config directly), so this is unconditional.
  • orch.go: InstallSkillsBothInstallSkillsAll (now three backends); /skills status gains an oc column.
  • Tests: TestMaterializeOpenCode (dir, frontmatter, no allowed-tools, no openai.yaml) + TestCatalogNamesAreOpenCodeCompatible (all 27 names satisfy opencode's stricter regex ^[a-z0-9]+(-[a-z0-9]+)*).

Verification

  • go build ./...
  • go test ./internal/skills/ ./internal/setup/ ./internal/repl/ ✅ (all pass, incl. 2 new tests)

Notes / coverage

  • opencode also auto-discovers ~/.claude/skills, so dual cc+opencode users already had skills via the Claude-compatible path. This native materialization ensures opencode-only users get the catalog too, and keeps it authoritative in opencode's own dir.
  • All catalog skill names are already opencode-compliant (no underscores); a regression test now guards that.

Risk

LOW — additive backend; existing cc/codex paths unchanged. The one behavioral change is that /skills install now also writes to ~/.config/opencode/skills/ (previously only cc+codex).

The opencode backend (1.20.8, #141) routes skill install through the
generic InstallSkillsReport(result.Backend) call in the /orch picker, but
the skills materializer only knew about "cc" and "codex". Activating
opencode crashed with: skills: unknown backend "opencode".

Add opencode as a first-class skills backend:
- BackendOpenCode writes to ~/.config/opencode/skills/<name>/SKILL.md
  (opencode's global discovery dir), rendering name+description only —
  opencode ignores allowed-tools and has no openai.yaml sibling.
- main.go: sync skills on every opencode activation (idempotent), matching
  the cc/codex startup sync. opencode has no global-install prompt, so this
  is unconditional.
- InstallSkillsBoth -> InstallSkillsAll (now three backends); /skills
  status gains an "oc" column.
- Tests: opencode materialization + a guard that all catalog names satisfy
  opencode's stricter name regex (no underscores).
@sigilix

sigilix Bot commented Jul 13, 2026

Copy link
Copy Markdown

Sigilix Overview

Effort: 2/5 (small)

Quality gates

  • ✅ PR title follows convention
  • ✅ PR description is complete
  • ℹ️ PR is linked to an issue — No Closes #N / Closes SIG-N keyword found in PR body or commit messages.

Summary — latest push

Adds first-class opencode backend support to the skills materializer so /orch activation no longer crashes with 'unknown backend', and wires unconditional skill sync on opencode activation since it bypasses the global-install consent prompt. The rename from InstallSkillsBoth to InstallSkillsAll reflects the expansion to three backends, and the /skills status command gains an opencode column. Existing cc/codex paths are untouched, making this a low-risk additive change.

Important files

File Score Notes Next step
internal/repl/repl.go 4/5 Renames InstallSkillsBoth to InstallSkillsAll and adds an unconditional opencode skills sync path that bypasses the GlobalInstall gate for the opencode backend. Add a targeted unit test verifying that InstallSkillsReport is called for opencode even when GlobalInstall is false, to prevent future regressions in consent-gating logic.
internal/skills/materialize_test.go 4/5 Adds TestMaterializeOpenCode validating directory layout, frontmatter, and the absence of allowed-tools and openai.yaml, plus TestCatalogNamesAreOpenCodeCompatible enforcing opencode's stricter name regex. Add a negative test case in TestCatalogNamesAreOpenCodeCompatible that asserts a name with underscores fails the regex, confirming the regex itself is correct.
internal/skills/materialize.go 4/5 Registers BackendOpenCode, maps it to ~/.config/opencode/skills, and adds a renderSkillMD case that omits allowed-tools to match opencode's recognized schema. Add a unit test verifying that an unrecognized backend string passed to Materialize returns the expected error, ensuring the default branch remains guarded.
internal/setup/orch.go 3/5 Renames InstallSkillsBoth to InstallSkillsAll and adds the opencode backend to the iteration, plus updates PrintSkillsStatus to include the oc column and directory. Verify the formatted status string alignment handles the new oc: column gracefully across varying terminal widths or when marks are missing.
main.go 3/5 Adds an unconditional setup.InstallSkills("opencode") call during opencode backend activation, mirroring the cc/codex sync but bypassing the global-install consent gate. Log or surface the ignored error from the InstallSkills call (currently discarded with _, _) so that disk-permission failures aren't silently swallowed during startup.

Confidence: 4/5

The change is an additive backend extension with no modifications to existing cc/codex paths, and the unconditional sync is correctly scoped to opencode's known consent-bypass behavior.

  • Verify the result.Backend == "opencode" condition at line 540 correctly short-circuits only for opencode and doesn't accidentally bypass GlobalInstall for cc/codex if a future backend string is mistyped.
  • Confirm that InstallSkillsAll in the /skills install handler (line 341) gracefully handles the opencode backend without attempting to write unsupported openai.yaml or allowed-tools frontmatter.
  • Ensure the unconditional opencode sync doesn't cause unexpected filesystem writes or UI noise on every /orch activation if the skills directory is already up-to-date.

Suggested labels: bug


Posted · 864abfc · 0 findings — View review
Dismiss @sigilix dismiss <reason> (not-a-bug | bad-anchor | already-covered | too-minor | wrong-context) · Re-run /sigilix review
Sigilix · 4 of 50 reviews used in past 5h

@sigilix sigilix Bot added the bug Something isn't working label Jul 13, 2026
@qualitymaxapp

qualitymaxapp Bot commented Jul 13, 2026

Copy link
Copy Markdown

✅ QualityMax Pipeline

Gate Result
🔍 AI Review ✅ Clean
🧪 Repo Tests ✅ 438/438 passed (go)
🤖 AI Tests ✅ 47/51 passed

Powered by QualityMax — AI-Powered Test Automation

@sigilix sigilix Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 finding outside the diff
File Scope Finding
internal/skills/materialize.go:45 file-scope Missing test for unknown backend error path in SkillsDir

Comment thread main.go Outdated
// Skills sync every launch (idempotent). opencode has no global-install
// prompt — qmax manages its config directly — so this is unconditional,
// unlike the OrchGlobalInstall-gated cc/codex sync above.
_, _ = setup.InstallSkills("opencode")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 TESTINGGROUNDED No test for opencode skills sync in main startup

The PR adds an unconditional skills sync for opencode backend at line 443 (_, _ = setup.InstallSkills("opencode")). This is a new runtime behavior that is not covered by any test in the diff. The existing tests for InstallSkills do not verify that opencode backend triggers the sync correctly.

More Info
  • Threat model: If the sync fails silently (due to the ignored error), opencode users may not receive the skill catalog, leading to missing functionality. The lack of a test means regressions in the integration between main startup and the skills materialization could go unnoticed.
  • Specific code citations: Line 443 in main.go: _, _ = setup.InstallSkills("opencode").
  • Existing protections: The InstallSkills function is tested via the materialize unit tests, but there is no integration test that exercises the opencode backend activation path in main.go.
  • Proposed mitigation: Add an integration test that simulates opencode backend activation and verifies skills are materialized, or at least ensure the InstallSkills call is covered in existing test suites for the opencode backend.
  • Alternative mitigations considered: Assume the unit tests for Materialize are sufficient, but they do not cover the call site in main.go or the error handling (ignored error).
  • Severity calibration: Score 3 because the behavior is idempotent and additive; a failure would degrade user experience but not break core functionality. However, it's a new runtime path that should be tested.
Prompt To Fix With AI
This is a comment left during a code review.
Path: main.go
Line: 446

Comment:
**No test for opencode skills sync in main startup**

The PR adds an unconditional skills sync for opencode backend at line 443 (`_, _ = setup.InstallSkills("opencode")`). This is a new runtime behavior that is not covered by any test in the diff. The existing tests for `InstallSkills` do not verify that opencode backend triggers the sync correctly.

Threat model:
If the sync fails silently (due to the ignored error), opencode users may not receive the skill catalog, leading to missing functionality. The lack of a test means regressions in the integration between main startup and the skills materialization could go unnoticed.

Specific code citations:
Line 443 in main.go: `_, _ = setup.InstallSkills("opencode")`.

Existing protections:
The `InstallSkills` function is tested via the materialize unit tests, but there is no integration test that exercises the opencode backend activation path in main.go.

Proposed mitigation:
Add an integration test that simulates opencode backend activation and verifies skills are materialized, or at least ensure the `InstallSkills` call is covered in existing test suites for the opencode backend.

Alternative mitigations considered:
Assume the unit tests for `Materialize` are sufficient, but they do not cover the call site in main.go or the error handling (ignored error).

Severity calibration:
Score 3 because the behavior is idempotent and additive; a failure would degrade user experience but not break core functionality. However, it's a new runtime path that should be tested.

How can I resolve this? If you propose a fix, please make it concise.

case BackendOpenCode:
return filepath.Join(home, ".config", "opencode", "skills"), nil
default:
return "", fmt.Errorf("skills: unknown backend %q", backend)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 SECURITYGROUNDED New backend constant expands attack surface for path traversal validation

Adding BackendOpenCode introduces a new path ~/.config/opencode/skills that Materialize will write to. The existing ensureWithinHome guard and mkdirStrict permissions (0600/0700) still apply, but any future changes to path validation or symlink checks must consider this new backend. The risk is low because the same security controls are reused, but the expansion increases the code surface that must remain consistent.

Remediation: Resolve the path and confirm it stays within an allowed base directory (reject .. and absolute paths); prefer an allow-list of known-safe names over user-supplied paths.

More Info
  • Threat model: An attacker could attempt to exploit a symlink or path traversal if the validation logic is later modified or bypassed for the opencode backend. The impact is local file write outside the intended directory.
  • Specific code citations: SkillsDir case for BackendOpenCode returns filepath.Join(home, ".config", "opencode", "skills"). Materialize calls ensureWithinHome and mkdirStrict.
  • Existing protections: ensureWithinHome resolves symlinks and checks that the final path is within the user's home directory. mkdirStrict sets owner-only permissions.
  • Proposed mitigation: Ensure any future changes to path validation or directory creation apply uniformly to all backends, including opencode.
  • Alternative mitigations considered: Consider adding a backend-specific validation hook, but the current uniform approach is sufficient.
  • Severity calibration: Score 2 because it's a hardening gap that widens the attack surface slightly; the existing guards are robust and the new backend follows the same pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/skills/materialize.go
Line: 45

Comment:
**New backend constant expands attack surface for path traversal validation**

Adding `BackendOpenCode` introduces a new path `~/.config/opencode/skills` that `Materialize` will write to. The existing `ensureWithinHome` guard and `mkdirStrict` permissions (0600/0700) still apply, but any future changes to path validation or symlink checks must consider this new backend. The risk is low because the same security controls are reused, but the expansion increases the code surface that must remain consistent.

Threat model:
An attacker could attempt to exploit a symlink or path traversal if the validation logic is later modified or bypassed for the opencode backend. The impact is local file write outside the intended directory.

Specific code citations:
`SkillsDir` case for `BackendOpenCode` returns `filepath.Join(home, ".config", "opencode", "skills")`. `Materialize` calls `ensureWithinHome` and `mkdirStrict`.

Existing protections:
`ensureWithinHome` resolves symlinks and checks that the final path is within the user's home directory. `mkdirStrict` sets owner-only permissions.

Proposed mitigation:
Ensure any future changes to path validation or directory creation apply uniformly to all backends, including opencode.

Alternative mitigations considered:
Consider adding a backend-specific validation hook, but the current uniform approach is sufficient.

Severity calibration:
Score 2 because it's a hardening gap that widens the attack surface slightly; the existing guards are robust and the new backend follows the same pattern.

How can I resolve this? If you propose a fix, please make it concise.

…balInstall

The /orch picker gated InstallSkillsReport behind consent.GlobalInstall,
which opencode never sets (its consent flow skips the global-install
prompt). So opencode-only users activating via /orch got no skills, even
though WriteOpenCodeConfig already ran unconditionally in the same flow
and main.go syncs opencode skills on every startup.

Decouple the skills install from the GlobalInstall gate for opencode only:
cc/codex keep their explicit opt-in; opencode matches its managed-config
model (and main.go). RunOrch stays gated — IsOrchInstalled("opencode")
already returns true, so it never runs for opencode anyway.
…ves; surface startup error

Address review (sigilix):
- TestSkillsDirRejectsUnknownBackend: assert invalid backends error and
  that Materialize propagates it (guards the default branch).
- TestOpenCodeNameRegexRejectsInvalid: underscores, leading/trailing/double
  hyphens, uppercase, empty all rejected — confirms the regex itself.
- main.go: log the opencode InstallSkills error to stderr instead of
  discarding it, matching the adjacent WriteOpenCodeConfig warning.
@Desperado Desperado merged commit 513583b into main Jul 13, 2026
7 checks passed
@Desperado Desperado mentioned this pull request Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant