Skip to content

Conversation

@dairycow
Copy link

@dairycow dairycow commented Nov 6, 2025

Motivation and Context

How Has This Been Tested?

Breaking Changes

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Summary by CodeRabbit

  • Documentation

    • Added an installation guide with step-by-step setup and verification for opencode superpowers.
    • Added a comprehensive bootstrap guide covering workflow, conventions, naming, tool usage, and skill management policies.
  • New Features

    • Added a command-line bootstrap tool to initialize the system, list/discover available skills, and load/use skills with clear scope, precedence, and user-facing output.

@coderabbitai
Copy link

coderabbitai bot commented Nov 6, 2025

Walkthrough

Adds opencode installation and bootstrap documentation, a new comprehensive bootstrap CLI module (bootstrap/superpowers-bootstrap.js) that discovers, parses, and loads skills with namespace precedence and git-update checks, and lightweight wrapper entrypoints for opencode and codex.

Changes

Cohort / File(s) Summary
Documentation
.opencode/INSTALL.md, .opencode/superpowers-bootstrap.md
Adds an installation guide and a detailed bootstrap policy document describing setup steps, verification, skill naming/conventions, required announcements, per-skill checklists, and mandatory workflows.
Bootstrap CLI Module
bootstrap/superpowers-bootstrap.js
New Node.js CLI implementing bootstrap, find-skills, and use-skill; detects opencode/codex context, performs git fetch/status with a 3s timeout, recursively discovers SKILL.md, extracts YAML-like frontmatter (name/description/when_to_use), prints metadata and content, enforces personal-over-superpowers precedence, and auto-loads a default bootstrap skill.
Lightweight Wrappers
.opencode/superpowers-opencode, .codex/superpowers-codex
Replace prior heavy CLI logic with simple wrappers that delegate execution to the shared bootstrap module (../bootstrap/superpowers-bootstrap.js).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Wrapper as entry script
    participant Bootstrap as bootstrap/superpowers-bootstrap.js
    participant Git
    participant FS as Filesystem

    User->>Wrapper: bootstrap
    Wrapper->>Bootstrap: invoke (context detected)
    Bootstrap->>Git: git fetch/status (3s timeout)
    alt remote behind
        Git-->>Bootstrap: updates available
        Bootstrap-->>User: show update notice
    else up-to-date / timeout
        Git-->>Bootstrap: ok / non-blocking error
    end
    Bootstrap->>FS: read bootstrap markdown & discover SKILL.md (personal then superpowers)
    FS-->>Bootstrap: bootstrap doc + skill list (frontmatter + content)
    Bootstrap-->>User: display bootstrap instructions and skills

    User->>Wrapper: use-skill [name]
    Wrapper->>Bootstrap: invoke use-skill
    Bootstrap->>FS: resolve skill (personal namespace preferred)
    alt personal found
        FS-->>Bootstrap: personal skill content
    else superpowers found
        FS-->>Bootstrap: superpowers skill content
    else not found
        Bootstrap-->>User: error + available skills
    end
    Bootstrap-->>User: print skill metadata and content (frontmatter separated)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Files needing extra attention:
    • bootstrap/superpowers-bootstrap.js — control flow, error handling, git timeout behavior, and CLI parsing.
    • Frontmatter extraction and content-splitting logic for edge cases (malformed frontmatter, encoding).
    • Skill discovery recursion and depth/permission assumptions (performance on large trees).
    • Namespace/precedence resolution and the wrapper delegation correctness.

Possibly related PRs

  • Add personal superpowers overlay system #2 — Implements similar skill-discovery/bootstrap CLI entrypoints, frontmatter parsing, and personal-vs-superpowers precedence logic; likely overlapping functionality and design decisions.

Poem

🐰 I cloned the superpowers just right,
I sniffed SKILL.md by lantern-light.
A bootstrap chant, a tiny hop,
Personal first, then powers next stop —
The rabbit loads skills and hops with delight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'init opencode bootstrap for use with opencode.ai' directly reflects the main change: initializing a bootstrap system for opencode. It is specific, concise, and accurately summarizes the primary objective of the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b187e75 and e721510.

📒 Files selected for processing (3)
  • .opencode/INSTALL.md (1 hunks)
  • .opencode/superpowers-bootstrap.md (1 hunks)
  • .opencode/superpowers-opencode (1 hunks)

Comment on lines 232 to 294
// Handle namespaced skill names
let actualSkillPath;
let forceSuperpowers = false;

if (skillName.startsWith('superpowers:')) {
// Remove the superpowers: namespace prefix
actualSkillPath = skillName.substring('superpowers:'.length);
forceSuperpowers = true;
} else {
actualSkillPath = skillName;
}

// Remove "skills/" prefix if present
if (actualSkillPath.startsWith('skills/')) {
actualSkillPath = actualSkillPath.substring('skills/'.length);
}

// Function to find skill file
function findSkillFile(searchPath) {
// Check for exact match with SKILL.md
const skillMdPath = path.join(searchPath, 'SKILL.md');
if (fs.existsSync(skillMdPath)) {
return skillMdPath;
}

// Check for direct SKILL.md file
if (searchPath.endsWith('SKILL.md') && fs.existsSync(searchPath)) {
return searchPath;
}

return null;
}

let skillFile = null;

// If superpowers: namespace was used, only check superpowers skills
if (forceSuperpowers) {
if (fs.existsSync(superpowersSkillsDir)) {
const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
skillFile = findSkillFile(superpowersPath);
}
} else {
// First check personal skills directory (takes precedence)
if (fs.existsSync(personalSkillsDir)) {
const personalPath = path.join(personalSkillsDir, actualSkillPath);
skillFile = findSkillFile(personalPath);
if (skillFile) {
console.log(`# Loading personal skill: ${actualSkillPath}`);
console.log(`# Source: ${skillFile}`);
console.log('');
}
}

// If not found in personal, check superpowers skills
if (!skillFile && fs.existsSync(superpowersSkillsDir)) {
const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
skillFile = findSkillFile(superpowersPath);
if (skillFile) {
console.log(`# Loading superpowers skill: superpowers:${actualSkillPath}`);
console.log(`# Source: ${skillFile}`);
console.log('');
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Block relative path escapes before resolving skill files

runUseSkill feeds actualSkillPath straight into path.join/findSkillFile, so names like ../.ssh/id_rsa will resolve outside the allowed skills roots and dump arbitrary files to stdout. This is a local path-traversal leak waiting to happen (including for superpowers-scoped names). Please normalize the requested path, reject absolute or .. segments, and only resolve within the intended base directory before reading.

Apply this diff to sanitize the path before resolution:

     if (skillName.startsWith('superpowers:')) {
         // Remove the superpowers: namespace prefix
         actualSkillPath = skillName.substring('superpowers:'.length);
         forceSuperpowers = true;
     } else {
         actualSkillPath = skillName;
     }

     // Remove "skills/" prefix if present
     if (actualSkillPath.startsWith('skills/')) {
         actualSkillPath = actualSkillPath.substring('skills/'.length);
     }
+
+    const normalizedSkillPath = path
+        .normalize(actualSkillPath)
+        .replace(/^([/\\])+/g, '');
+
+    if (
+        path.isAbsolute(actualSkillPath) ||
+        normalizedSkillPath.startsWith('..') ||
+        normalizedSkillPath.includes(`..${path.sep}`)
+    ) {
+        console.log('Error: Skill names must stay within the skills directories.');
+        return;
+    }
+
+    actualSkillPath = normalizedSkillPath;
🤖 Prompt for AI Agents
In .opencode/superpowers-opencode around lines 232-294, actualSkillPath is fed
directly into path.join/findSkillFile which allows path traversal (e.g. "../..")
and absolute paths; sanitize the value before any joins by: 1) rejecting
path.isAbsolute(actualSkillPath) and any path segments equal to '..' (split or
use path.normalize and check for leading ".." or occurrences of ".." after
normalization), 2) normalizing with path.normalize(actualSkillPath), 3)
computing the resolved candidate with path.resolve(baseDir, normalizedPath) for
each base (personalSkillsDir and superpowersSkillsDir) and then ensuring the
resolved path startsWith the intended baseDir (compare path.resolve(baseDir)
prefixes) before calling findSkillFile, and 4) log or throw a clear error and
skip lookup if the check fails; apply the same checks for both forceSuperpowers
and non-forced lookups so no absolute or traversing paths can escape the skill
roots.

@obra
Copy link
Owner

obra commented Nov 6, 2025

thanks so much for this contribution. It looks like it shares a ton of code with the codex bootstrap. Would you be up for refactoring so the two can share as much as possible?

@dairycow
Copy link
Author

dairycow commented Nov 6, 2025

@obra your blog post the other day sparked the idea, and I guess there's no reason why we can't apply to .opencode/.github. I have changed so that both call bootstrap/superpowers-bootstrap.js and still retain thin wrapper so that context is passed through

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6350ff2 and af39ae3.

📒 Files selected for processing (3)
  • .codex/superpowers-codex (1 hunks)
  • .opencode/superpowers-opencode (1 hunks)
  • bootstrap/superpowers-bootstrap.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .opencode/superpowers-opencode

Comment on lines +109 to +168
function findSkillsInDir(dir, sourceType, maxDepth = 1) {
const skills = [];

if (!fs.existsSync(dir)) return skills;

function searchDir(currentDir, currentDepth) {
if (currentDepth > maxDepth) return;

try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });

for (const entry of entries) {
if (entry.isDirectory()) {
const skillDir = path.join(currentDir, entry.name);
const skillFile = path.join(skillDir, 'SKILL.md');

if (fs.existsSync(skillFile)) {
skills.push(skillDir);
}

// For personal skills, search deeper (category/skill structure)
if (sourceType === 'personal' && currentDepth < maxDepth) {
searchDir(skillDir, currentDepth + 1);
}
}
}
} catch (error) {
// Ignore permission errors or other issues
}
}

searchDir(dir, 0);
return skills;
}

// Commands
function runFindSkills() {
console.log('Available skills:');
console.log('==================');
console.log('');

const foundSkills = new Set();

// Find personal skills first (these take precedence)
const personalSkills = findSkillsInDir(personalSkillsDir, 'personal', 2);
for (const skillPath of personalSkills) {
const relPath = path.relative(personalSkillsDir, skillPath);
foundSkills.add(relPath);
printSkill(skillPath, 'personal');
}

// Find superpowers skills (only if not already found in personal)
const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 'superpowers', 1);
for (const skillPath of superpowersSkills) {
const relPath = path.relative(superpowersSkillsDir, skillPath);
if (!foundSkills.has(relPath)) {
printSkill(skillPath, 'superpowers');
}
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Superpowers discovery stops at the category directories

Because we only recurse when sourceType === 'personal', the crawl never enters skills/<category>/<skill> inside the shared superpowers repo (e.g. skills/meta/using-superpowers). As a result find-skills prints zero superpowers entries and runUseSkill can’t locate them either—a complete functional regression. Please remove the source-type guard and give both trees the same depth budget so we actually index the nested skills.

Apply this diff to restore discovery:

-function findSkillsInDir(dir, sourceType, maxDepth = 1) {
+function findSkillsInDir(dir, maxDepth = 1) {
@@
-                    // For personal skills, search deeper (category/skill structure)
-                    if (sourceType === 'personal' && currentDepth < maxDepth) {
+                    if (currentDepth < maxDepth) {
                         searchDir(skillDir, currentDepth + 1);
                     }
@@
-    const personalSkills = findSkillsInDir(personalSkillsDir, 'personal', 2);
+    const personalSkills = findSkillsInDir(personalSkillsDir, 2);
@@
-    const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 'superpowers', 1);
+    const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 2);
🤖 Prompt for AI Agents
In bootstrap/superpowers-bootstrap.js around lines 109 to 168, the directory
search only recurses for sourceType === 'personal', causing shared superpowers
categories to never be traversed; remove the source-type guard so searchDir
recurses into subdirectories for all sources up to the provided maxDepth, and
call findSkillsInDir for both personal and superpowers with the same depth
budget (e.g., 2) so nested skills under category folders are discovered; keep
the SKILL.md check and error swallow intact.

Comment on lines +244 to +305
// Handle namespaced skill names
let actualSkillPath;
let forceSuperpowers = false;

if (skillName.startsWith('superpowers:')) {
// Remove the superpowers: namespace prefix
actualSkillPath = skillName.substring('superpowers:'.length);
forceSuperpowers = true;
} else {
actualSkillPath = skillName;
}

// Remove "skills/" prefix if present
if (actualSkillPath.startsWith('skills/')) {
actualSkillPath = actualSkillPath.substring('skills/'.length);
}

// Function to find skill file
function findSkillFile(searchPath) {
// Check for exact match with SKILL.md
const skillMdPath = path.join(searchPath, 'SKILL.md');
if (fs.existsSync(skillMdPath)) {
return skillMdPath;
}

// Check for direct SKILL.md file
if (searchPath.endsWith('SKILL.md') && fs.existsSync(searchPath)) {
return searchPath;
}

return null;
}

let skillFile = null;

// If superpowers: namespace was used, only check superpowers skills
if (forceSuperpowers) {
if (fs.existsSync(superpowersSkillsDir)) {
const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
skillFile = findSkillFile(superpowersPath);
}
} else {
// First check personal skills directory (takes precedence)
if (fs.existsSync(personalSkillsDir)) {
const personalPath = path.join(personalSkillsDir, actualSkillPath);
skillFile = findSkillFile(personalPath);
if (skillFile) {
console.log(`# Loading personal skill: ${actualSkillPath}`);
console.log(`# Source: ${skillFile}`);
console.log('');
}
}

// If not found in personal, check superpowers skills
if (!skillFile && fs.existsSync(superpowersSkillsDir)) {
const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
skillFile = findSkillFile(superpowersPath);
if (skillFile) {
console.log(`# Loading superpowers skill: superpowers:${actualSkillPath}`);
console.log(`# Source: ${skillFile}`);
console.log('');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Slug-based skill lookup is broken

The bootstrap tells agents to run superpowers:skill-name, and runBootstrap() itself calls runUseSkill('superpowers:using-superpowers'). However the lookup only probes ~/.${context}/superpowers/skills/<slug>/SKILL.md, so anything stored under skills/<category>/<slug>/SKILL.md (which is how the repo is structured) throws “Skill not found”. Please add a slug resolver that falls back to scanning the indexed directories so the canonical names keep working.

Apply this diff to restore slug resolution:

 function findSkillsInDir(dir, maxDepth = 1) {
@@
 }
 
+function resolveSkillBySlug(baseDir, slug, maxDepth = 2) {
+    const direct = findSkillFile(path.join(baseDir, slug));
+    if (direct) {
+        return direct;
+    }
+
+    const candidates = findSkillsInDir(baseDir, maxDepth);
+    for (const skillDir of candidates) {
+        if (path.basename(skillDir) === slug) {
+            return path.join(skillDir, 'SKILL.md');
+        }
+    }
+
+    return null;
+}
+
 function runUseSkill(skillName) {
@@
     if (forceSuperpowers) {
         if (fs.existsSync(superpowersSkillsDir)) {
-            const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
-            skillFile = findSkillFile(superpowersPath);
+            skillFile = resolveSkillBySlug(superpowersSkillsDir, actualSkillPath);
         }
     } else {
         // First check personal skills directory (takes precedence)
         if (fs.existsSync(personalSkillsDir)) {
-            const personalPath = path.join(personalSkillsDir, actualSkillPath);
-            skillFile = findSkillFile(personalPath);
+            skillFile = resolveSkillBySlug(personalSkillsDir, actualSkillPath);
             if (skillFile) {
                 console.log(`# Loading personal skill: ${actualSkillPath}`);
                 console.log(`# Source: ${skillFile}`);
                 console.log('');
             }
         }
 
         // If not found in personal, check superpowers skills
         if (!skillFile && fs.existsSync(superpowersSkillsDir)) {
-            const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
-            skillFile = findSkillFile(superpowersPath);
+            skillFile = resolveSkillBySlug(superpowersSkillsDir, actualSkillPath);
             if (skillFile) {
                 console.log(`# Loading superpowers skill: superpowers:${actualSkillPath}`);
                 console.log(`# Source: ${skillFile}`);
                 console.log('');
             }
         }
🤖 Prompt for AI Agents
In bootstrap/superpowers-bootstrap.js around lines 244 to 305, the current
lookup only checks superpowersSkillsDir/<slug>/SKILL.md (and same for personal)
which breaks canonical names stored as skills/<category>/<slug>/SKILL.md;
implement a slug resolver fallback: if findSkillFile(searchPath) returns null
and searchPath exists, recursively (or shallowly) scan its subdirectories (or
the indexed skills directory) for any SKILL.md whose directory name matches the
slug or whose path ends with /<category>/<slug>/SKILL.md and return the first
match; apply this fallback both when forceSuperpowers is true and when checking
personal then superpowers so canonical names like superpowers:category/slug or
just superpowers:slug resolve correctly, and keep existing logging when a file
is found.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
bootstrap/superpowers-bootstrap.js (2)

113-146: Superpowers skills in category directories won't be discovered.

The recursion guard on line 134 only allows deeper traversal for sourceType === 'personal', preventing the discovery of superpowers skills organized as skills/<category>/<skill>/SKILL.md. Combined with the maxDepth=1 call on line 165, this means superpowers skills nested in category directories will never be found.

Apply this diff to enable discovery for all skill sources:

-function findSkillsInDir(dir, sourceType, maxDepth = 1) {
+function findSkillsInDir(dir, maxDepth = 1) {
     const skills = [];
 
     if (!fs.existsSync(dir)) return skills;
 
     function searchDir(currentDir, currentDepth) {
         if (currentDepth > maxDepth) return;
 
         try {
             const entries = fs.readdirSync(currentDir, { withFileTypes: true });
 
             for (const entry of entries) {
                 if (entry.isDirectory()) {
                     const skillDir = path.join(currentDir, entry.name);
                     const skillFile = path.join(skillDir, 'SKILL.md');
 
                     if (fs.existsSync(skillFile)) {
                         skills.push(skillDir);
                     }
 
-                    // For personal skills, search deeper (category/skill structure)
-                    if (sourceType === 'personal' && currentDepth < maxDepth) {
+                    if (currentDepth < maxDepth) {
                         searchDir(skillDir, currentDepth + 1);
                     }
                 }
             }

And update the call sites:

-    const personalSkills = findSkillsInDir(personalSkillsDir, 'personal', 2);
+    const personalSkills = findSkillsInDir(personalSkillsDir, 2);
     for (const skillPath of personalSkills) {
         const relPath = path.relative(personalSkillsDir, skillPath);
         foundSkills.add(relPath);
         printSkill(skillPath, 'personal');
     }
 
     // Find superpowers skills (only if not already found in personal)
-    const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 'superpowers', 1);
+    const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 2);

238-376: Slug-based skill lookup doesn't handle category directories.

The direct path construction (lines 286, 292, 303) means skills organized as skills/<category>/<slug>/SKILL.md cannot be loaded by their slug alone. For example, superpowers:using-superpowers will fail if the skill is located at skills/meta/using-superpowers/SKILL.md.

Implement a fallback resolver that scans the indexed skills when direct lookup fails:

 function findSkillsInDir(dir, maxDepth = 1) {
     // ... existing implementation
 }
 
+function resolveSkillBySlug(baseDir, slug, maxDepth = 2) {
+    // Try direct path first
+    const directPath = path.join(baseDir, slug);
+    const direct = findSkillFile(directPath);
+    if (direct) {
+        return direct;
+    }
+
+    // Fall back to scanning for the slug in category subdirectories
+    const candidates = findSkillsInDir(baseDir, maxDepth);
+    for (const skillDir of candidates) {
+        if (path.basename(skillDir) === slug) {
+            return path.join(skillDir, 'SKILL.md');
+        }
+    }
+
+    return null;
+}
+
 function runUseSkill(skillName) {
     // ... namespace handling ...
     
     // If superpowers: namespace was used, only check superpowers skills
     if (forceSuperpowers) {
         if (fs.existsSync(superpowersSkillsDir)) {
-            const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
-            skillFile = findSkillFile(superpowersPath);
+            skillFile = resolveSkillBySlug(superpowersSkillsDir, actualSkillPath);
         }
     } else {
         // First check personal skills directory (takes precedence)
         if (fs.existsSync(personalSkillsDir)) {
-            const personalPath = path.join(personalSkillsDir, actualSkillPath);
-            skillFile = findSkillFile(personalPath);
+            skillFile = resolveSkillBySlug(personalSkillsDir, actualSkillPath);
             if (skillFile) {
                 console.log(`# Loading personal skill: ${actualSkillPath}`);
                 console.log(`# Source: ${skillFile}`);
                 console.log('');
             }
         }
 
         // If not found in personal, check superpowers skills
         if (!skillFile && fs.existsSync(superpowersSkillsDir)) {
-            const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
-            skillFile = findSkillFile(superpowersPath);
+            skillFile = resolveSkillBySlug(superpowersSkillsDir, actualSkillPath);
             if (skillFile) {
                 console.log(`# Loading superpowers skill: superpowers:${actualSkillPath}`);
                 console.log(`# Source: ${skillFile}`);
                 console.log('');
             }
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af39ae3 and a4edc81.

📒 Files selected for processing (1)
  • bootstrap/superpowers-bootstrap.js (1 hunks)
🔇 Additional comments (7)
bootstrap/superpowers-bootstrap.js (7)

9-21: LGTM! Context detection is well-implemented.

The auto-detection logic correctly identifies the context from the parent directory and validates that it starts with a dot, which aligns with the expected directory structure.


32-54: LGTM! Update check is robust.

The timeout protection and graceful error handling ensure that network issues or git errors don't block the bootstrap process. The parsing logic correctly identifies when the local branch is behind remote.


56-90: Simple frontmatter parser is adequate for the use case.

The implementation handles the expected single-line key-value format. The regex pattern \w+ correctly matches the underscore-separated keys used in the codebase (e.g., when_to_use), and the graceful error handling returns empty values if parsing fails.


92-111: LGTM! Skill printing logic is clear.

The namespace handling correctly distinguishes between personal skills (no prefix) and superpowers skills (superpowers: prefix), and the metadata display is well-formatted.


149-182: Skill listing logic is sound, contingent on discovery fix.

The precedence handling (personal skills override superpowers skills) and deduplication logic are correct. However, this function's effectiveness depends on fixing the discovery issue flagged in lines 113-146.


184-236: Bootstrap flow is well-structured.

The sequence (update check → bootstrap docs → available skills → auto-load default skill) provides a comprehensive initialization. However, the auto-loading on line 227 depends on the slug resolution fix flagged in the next segment.


378-404: LGTM! CLI dispatch is clean.

The command handling is straightforward and provides helpful usage information when no valid command is specified.

const homeDir = os.homedir();
const superpowersSkillsDir = path.join(homeDir, `.${context}`, 'superpowers', 'skills');
const personalSkillsDir = path.join(homeDir, `.${context}`, 'skills');
const bootstrapFile = path.join(homeDir, `.${context}`, 'superpowers', `.${context}`, 'superpowers-bootstrap.md');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the bootstrap file path construction.

The path construction embeds .${context} twice, creating a path like ~/.opencode/superpowers/.opencode/superpowers-bootstrap.md. This unusual nesting may be intentional if the repository structure mirrors the home directory layout, but it's worth confirming.

Run the following script to check if the bootstrap file exists at this location:


🏁 Script executed:

#!/bin/bash
# Description: Check if bootstrap file exists in the expected location with double context nesting

# Check the repository structure
fd -t f "superpowers-bootstrap.md" --max-depth 5

# Show the directory structure around superpowers-bootstrap.md
fd -t d "^\\.opencode$|^\\.codex$" --max-depth 3 -x echo "Directory: {}" \; -x fd -t f "superpowers-bootstrap.md" {} --max-depth 3

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Find bootstrap-related files and examine the code around line 28

# Search for the file with bootstrap in the name
fd -t f "bootstrap" -e js

# Look for the specific file mentioned in the review
cat bootstrap/superpowers-bootstrap.js | head -35

Length of output: 1445


Comment on lines +248 to +263
// Handle namespaced skill names
let actualSkillPath;
let forceSuperpowers = false;

if (skillName.startsWith('superpowers:')) {
// Remove the superpowers: namespace prefix
actualSkillPath = skillName.substring('superpowers:'.length);
forceSuperpowers = true;
} else {
actualSkillPath = skillName;
}

// Remove "skills/" prefix if present
if (actualSkillPath.startsWith('skills/')) {
actualSkillPath = actualSkillPath.substring('skills/'.length);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add input validation to prevent path traversal.

The actualSkillPath is derived from user input without validation. A malicious or accidental input like ../../.ssh/id_rsa could traverse outside the skills directory, and since the resolved file content is printed to stdout, this could leak sensitive files.

Apply this diff to add validation:

     } else {
         actualSkillPath = skillName;
     }
 
     // Remove "skills/" prefix if present
     if (actualSkillPath.startsWith('skills/')) {
         actualSkillPath = actualSkillPath.substring('skills/'.length);
     }
+
+    // Validate no path traversal attempts
+    if (actualSkillPath.includes('..') || actualSkillPath.startsWith('/')) {
+        console.log(`Error: Invalid skill path: ${actualSkillPath}`);
+        console.log('Skill paths cannot contain ".." or start with "/"');
+        return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle namespaced skill names
let actualSkillPath;
let forceSuperpowers = false;
if (skillName.startsWith('superpowers:')) {
// Remove the superpowers: namespace prefix
actualSkillPath = skillName.substring('superpowers:'.length);
forceSuperpowers = true;
} else {
actualSkillPath = skillName;
}
// Remove "skills/" prefix if present
if (actualSkillPath.startsWith('skills/')) {
actualSkillPath = actualSkillPath.substring('skills/'.length);
}
// Handle namespaced skill names
let actualSkillPath;
let forceSuperpowers = false;
if (skillName.startsWith('superpowers:')) {
// Remove the superpowers: namespace prefix
actualSkillPath = skillName.substring('superpowers:'.length);
forceSuperpowers = true;
} else {
actualSkillPath = skillName;
}
// Remove "skills/" prefix if present
if (actualSkillPath.startsWith('skills/')) {
actualSkillPath = actualSkillPath.substring('skills/'.length);
}
// Validate no path traversal attempts
if (actualSkillPath.includes('..') || actualSkillPath.startsWith('/')) {
console.log(`Error: Invalid skill path: ${actualSkillPath}`);
console.log('Skill paths cannot contain ".." or start with "/"');
return;
}

@obra
Copy link
Owner

obra commented Nov 8, 2025

i'm traveling for a family event through the long weekend, but I'm excited to check this out when I'm home and have a moment

@dairycow
Copy link
Author

All good, I was playing around with it a bit further over the past few days - the bootstrap is not quite the same. I like the idea but am cautious of the quality to continue with the PR.

Someone else has been working on a plugin https://github.com/malhashemi/opencode-skills/tree/main

Porting to Opencode or other Agentic IDEs may require more specific tinkering. Or potentially a use case for A2A protocol.

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.

2 participants