-
Notifications
You must be signed in to change notification settings - Fork 536
init opencode bootstrap for use with opencode.ai #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
📒 Files selected for processing (3)
.opencode/INSTALL.md(1 hunks).opencode/superpowers-bootstrap.md(1 hunks).opencode/superpowers-opencode(1 hunks)
.opencode/superpowers-opencode
Outdated
| // 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(''); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
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? |
|
@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 |
There was a problem hiding this 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
📒 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
| 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'); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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(''); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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 asskills/<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.mdcannot be loaded by their slug alone. For example,superpowers:using-superpowerswill fail if the skill is located atskills/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
📒 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'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 3Length 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
| // 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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; | |
| } |
|
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 |
|
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. |
Motivation and Context
How Has This Been Tested?
Breaking Changes
Types of changes
Checklist
Additional context
Summary by CodeRabbit
Documentation
New Features