feat: skill expansion, detection, and SKILL.md sidebar preview#152
feat: skill expansion, detection, and SKILL.md sidebar preview#152
Conversation
Expand /skill-name slash commands into full SKILL.md instructions server-side so non-Claude providers receive actionable procedures instead of opaque slash commands they cannot interpret. Sub-skill references are resolved into a lookup table so the model can read each sub-skill file on demand (progressive expansion).
Lock permission mode to bypass for autoresearch scenarios across all providers (Gemini, Codex, OpenRouter). Show provider-specific permission mode labels in the chat input controls.
- Expand SKILL_MD_PATH_RE to match .agents/, .gemini/, .claude/ dirs - Detect skillExpander "# Skill: name" heading in isSkillContent messages - Track skills from Bash and FileChanges tool events (Codex uses these instead of Read/Write events) - Store SKILL.md file paths in SessionContextTaskItem for preview support
Make skill items in the sidebar clickable to preview their SKILL.md content in a modal. Works for all providers (Claude, Codex, Gemini). - Add GET /api/skills/resolve endpoint to find and read SKILL.md by name - Add preloadedContent prop to ChatContextFilePreview for skills outside the project root (e.g. ~/.claude/skills/) - Add handleSkillPreview in sidebar: uses stored path when available, falls back to API resolution
bbsngg
left a comment
There was a problem hiding this comment.
@Zhang-Henry Nice work on the skill expansion feature — the core idea of expanding /skill-name into full SKILL.md content for non-Claude providers is solid. A few issues to address before merging:
🔴 Security
1. Auto-bypass permissions needs user consent
useChatComposerState.ts:844, ChatComposer.tsx:636
Autoresearch workflows silently force bypassPermissions mode with no user confirmation and the mode switch button is disabled. This is a significant security concern — granting full tool execution without explicit user opt-in.
Suggestion: Require a one-time confirmation dialog (or make it a user preference) before engaging bypass mode. At minimum, show a visible indicator that permissions are elevated.
2. /api/skills/resolve — workingDir parameter not validated
server/routes/skills.js:233
The name parameter is checked for path traversal, but workingDir is not validated against known project roots. An attacker could pass an arbitrary workingDir to probe the filesystem for SKILL.md files in unexpected locations.
const effectiveWorkingDir = (typeof workingDir === 'string' && workingDir.trim()) || process.cwd();Suggestion: Validate workingDir against known/registered project paths, or require it to be a project the current user owns.
🟡 Code Quality
3. Duplicated autoresearch check — extract to shared utility
The condition attachedPrompt?.scenarioId?.startsWith('autoresearch-') is duplicated in:
useChatComposerState.ts:844ChatComposer.tsx:636
This will diverge over time. Extract to a small helper like isAutoResearchScenario(scenarioId).
4. Misplaced export statement
server/utils/skillExpander.js:118
export { findSkillMdPath } is wedged between the JSDoc and expandSkillCommand's function definition. Move it to the top or bottom of the file for clarity.
5. Over-cautious null-safe chaining
server/openai-codex.js, server/openrouter.js
command?.trim?.() || command?.trim?.() is redundant — .trim is always a function on strings. Simplify to command?.trim() || command, or just pass command since expandSkillCommand already handles null.
6. Sequential sub-skill resolution — use Promise.all
server/utils/skillExpander.js:89-104
buildSubSkillIndex sequentially stats each sub-skill. Use Promise.all to parallelize for better latency when there are many references.
🔵 Minor
- package-lock.json: Large number of
"peer": trueadditions/removals look unrelated to this feature. Was this from annpm installon a different Node version? Consider reverting these noise changes. splitContextPrefixwhile-loop could be simplified to a single greedy regex match.ChatContextSidebar.tsx:598— whenresolveSkillfails, falling back tosetPreviewTasksilently hides the error. A toast notification would improve UX.
Overall the feature implementation is clean and well-structured. Requesting changes primarily for the security items (1 & 2).
- Security: validate workingDir against FORBIDDEN_PATHS in /api/skills/resolve
- Security: keep mode switch enabled when autoresearch bypass is active, add visible "Auto" indicator
- Extract isAutoResearchScenario() shared helper to eliminate duplication
- Move misplaced `export { findSkillMdPath }` to end of skillExpander.js
- Simplify command?.trim?.() to command?.trim() in codex/openrouter
- Parallelize buildSubSkillIndex with Promise.all
- Simplify splitContextPrefix to single greedy regex match
- Add console.warn on skill resolve failure in sidebar
- Revert unrelated package-lock.json peer:true noise
Zhang-Henry
left a comment
There was a problem hiding this comment.
Thanks for the thorough review @bbsngg! All items addressed in 8e6c10d:
🔴 Security
1. Auto-bypass permissions — Mode switch button is no longer disabled when autoresearch is attached, so the user can override the bypass. Added a visible amber "Auto" badge next to the controls to indicate elevated permissions. The auto-bypass behavior itself is kept (autoresearch workflows need it to function), but it's now transparent and overridable.
2. workingDir validation — Added validation against FORBIDDEN_PATHS (same list used by project path validation) and rejects non-absolute paths. When workingDir matches process.cwd() the check is skipped since that's the default fallback.
🟡 Code Quality
3. Duplicated autoresearch check — Extracted isAutoResearchScenario(scenarioId) into src/components/chat/utils/autoResearch.ts, used in both useChatComposerState.ts and ChatComposer.tsx.
4. Misplaced export — Moved export { findSkillMdPath } to the end of skillExpander.js, after expandSkillCommand.
5. Over-cautious null-safe chaining — Simplified to command?.trim() in both openai-codex.js and openrouter.js.
6. Sequential sub-skill resolution — Converted buildSubSkillIndex to use Promise.all for parallel stat calls.
🔵 Minor
- package-lock.json — Reverted the
peer: truenoise (was from a different npm version). splitContextPrefix— Replaced the while-loop with a single greedy regex:/^(\s*\[Context:[^\]]*\]\s*)+/i.ChatContextSidebar.tsx— Addedconsole.warnfor both non-ok response and catch block. No toast infrastructure exists in the project currently — can add that as a follow-up if desired.
Summary
/skill-namecommands, instead of opaque slash commands.agents/,.gemini/,.claude/directories, from skillExpander heading format, and from Bash/FileChanges events (critical for Codex which doesn't emit Read tool events)Test plan
/skill-namein Codex session → verify skill is expanded and detected in sidebar/skill-namein Gemini session → verify skill is detected fromread_filetool.gemini/skills/and.agents/skills/are detectedGET /api/skills/resolve?name=xxxreturns 404 for nonexistent and 400 for traversal attemptsnpx tsc --noEmit— passes clean