feat: add Codex skill picker#581
Conversation
There was a problem hiding this comment.
Review mode: initial
Findings:
- [Major] Hub skill discovery skips the default
~/.codex/skillsuser-skill root unlessCODEX_HOMEis set, so Codex skills disappear for the common default install path. The CLI-side scanner already treats~/.codex/skillsas a user root, so the web picker and local agent will disagree here. - [Minor] The picker clears
loadedSkillson any refresh failure, which turns a transient read/network error into an empty dialog even when cached skills were already available.
Testing: Not run (review only).
|
|
||
| function getUserSkillsRoots(options: CodexSkillScanOptions): string[] { | ||
| const roots = [join(getHomeDirectory(options), '.agents', 'skills')] | ||
| const codexHome = options.codexHome ?? process.env.CODEX_HOME |
There was a problem hiding this comment.
[Major] getUserSkillsRoots() only appends CODEX_HOME/skills when CODEX_HOME is present, so the default ~/.codex/skills directory is never scanned. That drops user Codex skills for the common default setup.
Suggested fix:
function getUserSkillsRoots(options: CodexSkillScanOptions): string[] {
return [
join(getHomeDirectory(options), '.agents', 'skills'),
join(getCodexHome(options), 'skills'),
]
}| void refreshSkills().then((nextSkills) => { | ||
| if (cancelled) return | ||
| setLoadedSkills(nextSkills) | ||
| }).catch(() => { |
There was a problem hiding this comment.
[Minor] This catch path replaces the existing list with [], so a transient refresh failure makes the picker empty even if skills already contained cached results. Keeping the previous list preserves a usable dialog when the refresh fails.
Suggested fix:
void refreshSkills().then((nextSkills) => {
if (cancelled) return
setLoadedSkills(nextSkills)
}).catch(() => {
// keep the existing loadedSkills
})There was a problem hiding this comment.
Review mode: initial
Findings
-
[Major] Default Codex user skills are skipped —
getUserSkillsRootsonly addsCODEX_HOME/skillswhen the hub process exportsCODEX_HOME, so the normal~/.codex/skillslocation is omitted even though the existing CLI scanner treats it as a user root (cli/src/modules/common/skills.ts:24). This makes the web picker disagree with local Codex skill discovery and can show no user skills for common installs. Evidence:hub/src/skills/codexSkills.ts:90
Suggested fix:function getUserSkillsRoots(options: CodexSkillScanOptions): string[] { const home = getHomeDirectory(options) const codexHome = getCodexHome(options) return [ join(home, '.agents', 'skills'), join(codexHome, 'skills'), ] }
-
[Major] Plugin manifests can escape the plugin root —
resolve(pluginPath, skillsValue)accepts absolute paths and..traversal without checking that the resultingskillsDirstays under the plugin directory. A malformed plugin cache entry can make the hub scan unrelated directories forSKILL.mdfiles and expose their bounded metadata through the web API. Evidence:hub/src/skills/codexSkills.ts:272
Suggested fix:import { isAbsolute, relative } from 'node:path' const skillsDir = resolve(pluginPath, skillsValue) const rel = relative(pluginPath, skillsDir) if (rel.startsWith('..') || isAbsolute(rel)) { return null } return { name: manifest.name.trim(), skillsDir, pluginPath, }
Summary
Review mode: initial
The new skill picker path has two hub-side discovery issues: common Codex user skills can be missed, and plugin manifests can redirect scanning outside the plugin directory.
Testing
- Not run (bun unavailable in workspace)
HAPI Bot
There was a problem hiding this comment.
Findings
-
[Major] Desktop Codex sessions cannot open the skill picker — the composer now filters
$out of active autocomplete prefixes atweb/src/components/AssistantChat/HappyComposer.tsx:196, so the new skill button is the only picker entry point; that button is added withsm:hiddenatweb/src/components/AssistantChat/ComposerButtons.tsx:355, which hides it at desktop/tablet widths.
Suggested fix:className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-fg)]/60 transition-colors hover:bg-[var(--app-bg)] hover:text-[var(--app-fg)] disabled:cursor-not-allowed disabled:opacity-50"
-
[Major] Default Codex user skills are skipped —
getUserSkillsRootsonly addsCODEX_HOME/skillswhenCODEX_HOMEis explicitly set athub/src/skills/codexSkills.ts:90, so the normal~/.codex/skillsroot is missed even thoughgetCodexHomealready defines that default.
Suggested fix:function getUserSkillsRoots(options: CodexSkillScanOptions): string[] { const home = getHomeDirectory(options) const codexHome = getCodexHome(options) return [ join(home, '.agents', 'skills'), join(codexHome, 'skills'), ] }
-
[Major] Plugin manifests can escape the plugin root —
resolve(pluginPath, skillsValue)accepts absolute paths and..traversal athub/src/skills/codexSkills.ts:272without checking that the resulting skills directory remains inside the plugin directory, so a malformed cached plugin manifest can make the hub scan unrelated directories and expose their skill metadata through the web API.
Suggested fix:import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path' const skillsDir = resolve(pluginPath, skillsValue) const relativeSkillsDir = relative(pluginPath, skillsDir) if (relativeSkillsDir.startsWith('..') || isAbsolute(relativeSkillsDir)) { return null } return { name: manifest.name.trim(), skillsDir, pluginPath, }
Questions
- None.
Summary
Review mode: follow-up after new commits
The latest head still has the two hub discovery issues from the earlier pass, and the follow-up UI change removes the desktop entry point for the new Codex skill picker.
Testing
- Not run:
bunis unavailable in this runner (/bin/bash: bun: command not found).
HAPI Bot
There was a problem hiding this comment.
Findings
-
[Major] Desktop Codex sessions cannot open the skill picker —
HappyComposerremoves$from compact autocomplete prefixes, so typing$no longer opens autocomplete, and this new button is hidden at desktop/tablet widths bysm:hiddenatweb/src/components/AssistantChat/ComposerButtons.tsx:366. That leaves desktop Codex users with no entry point for the new picker.
Suggested fix:className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-fg)]/60 transition-colors hover:bg-[var(--app-bg)] hover:text-[var(--app-fg)] disabled:cursor-not-allowed disabled:opacity-50"
-
[Major] Default Codex user skills are skipped —
getUserSkillsRootsonly addsCODEX_HOME/skillswhenCODEX_HOMEis explicitly set, so the normal default fromgetCodexHome()(~/.codex) is used for plugin cache but not for~/.codex/skills; users with default Codex installs will not see their user skills. Evidencehub/src/skills/codexSkills.ts:90.
Suggested fix:function getUserSkillsRoots(options: CodexSkillScanOptions): string[] { const home = getHomeDirectory(options) const codexHome = getCodexHome(options) return [ join(home, '.agents', 'skills'), join(codexHome, 'skills'), ] }
-
[Major] Plugin manifests can escape the plugin root —
resolve(pluginPath, skillsValue)accepts absolute paths and..traversal without checking the resolved directory, so a malformed cached plugin manifest can make the hub scan unrelated directories and expose their skill names/descriptions/paths through/api/sessions/:id/skills. Evidencehub/src/skills/codexSkills.ts:272.
Suggested fix:import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path' const skillsDir = resolve(pluginPath, skillsValue) const relativeSkillsDir = relative(pluginPath, skillsDir) if (relativeSkillsDir.startsWith('..') || isAbsolute(relativeSkillsDir)) { return null } return { name: manifest.name.trim(), skillsDir, pluginPath, }
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The current head still contains the two hub discovery issues from the earlier pass, and the UI still hides the only new skill-picker entry point on desktop/tablet after
$autocomplete was disabled.
Testing
- Not run (automation).
HAPI Bot
Inline comments were omitted because GitHub returned HTTP 422 for valid diff anchors on this run.
There was a problem hiding this comment.
Findings
-
[Blocker] Skill discovery now scans the hub filesystem instead of the session machine — this route replaced the existing session RPC with
scanCodexSkillsForSession(session.metadata.path), butmetadata.pathis the CLI/session working directory and HAPI supports CLI machines connecting to a hub over Socket.IO. When the hub and agent are not the same process/filesystem,/api/sessions/:id/skillsreturns hub-local skills or nothing, and can expose hub-local paths unrelated to the session. Evidencehub/src/web/routes/sessions.ts:582.
Suggested fix:const result = await engine.listSkills(sessionResult.sessionId) return c.json(result.success ? result.skills ?? [] : [])
-
[Major] Default Codex user skills are skipped —
getCodexHome()correctly defaults to~/.codex, butgetUserSkillsRoots()only addsCODEX_HOME/skillswhen the env var is explicitly set. A normal Codex install with skills in~/.codex/skillswill not show those user skills. Evidencehub/src/skills/codexSkills.ts:90.
Suggested fix:function getUserSkillsRoots(options: CodexSkillScanOptions): string[] { const home = getHomeDirectory(options) const codexHome = getCodexHome(options) return [...new Set([ join(home, '.agents', 'skills'), join(codexHome, 'skills'), ])] }
-
[Major] Plugin manifests can escape the plugin root —
resolve(pluginPath, skillsValue)accepts absolute paths and..traversal from cached plugin manifests, so the skills endpoint can scan unrelated directories and return their skill names/descriptions/paths. Evidencehub/src/skills/codexSkills.ts:272.
Suggested fix:import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path' const skillsDir = resolve(pluginPath, skillsValue) const relativeSkillsDir = relative(pluginPath, skillsDir) if (relativeSkillsDir.startsWith('..') || isAbsolute(relativeSkillsDir)) { return null }
-
[Major] Typing
$no longer opens the Codex skill picker —$is removed from the active autocomplete prefixes, and the only new picker button is hidden atsmand wider breakpoints. Desktop/tablet Codex sessions therefore have no visible entry point, and the PR’s stated$trigger path is disabled. Evidenceweb/src/components/AssistantChat/HappyComposer.tsx:200.
Suggested fix:const handleChange = useCallback((e: ReactChangeEvent<HTMLTextAreaElement>) => { const selection = { start: e.target.selectionStart, end: e.target.selectionEnd } const text = e.target.value setInputState({ text, selection }) const dollarWord = findActiveWord(text, selection, ['$']) if (agentFlavor === 'codex' && dollarWord) { clearSuggestions() setSkillPickerAnchor({ text, selection, query: dollarWord.activeWord.slice(1), }) } }, [agentFlavor, clearSuggestions])
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The current head still has unresolved skill discovery regressions and adds a picker path that is not reachable from desktop/tablet or from the advertised
$typing flow.
Testing
- Not run (automation).
HAPI Bot
Inline comments were omitted because GitHub returned HTTP 422 for resolvable diff anchors on this run.
Summary
Codex sessions in HAPI Web can now discover local Codex skills and insert explicit
$skill-nameinvocations from a searchable picker. HAPI stays on the input-assist side of the boundary: it only reads bounded skill metadata, never expands fullSKILL.mdfiles, never runs skill scripts, and never auto-submits the composer.What Changed
GET /api/sessions/:id/skillsfor Codex sessions, returning an empty list for non-Codex sessions..codex-plugin/plugin.json, including invocation names likecompound-engineering:ce-plan.$compact autocomplete path with a larger searchable picker that supports keyboard and pointer selection.Testing
bun run typecheckbun run test:hubbun run test:webbun run build:web$opens the skill picker and inserts$compound-engineering:ce-planwithout sending; Claude$does not open the picker.中文说明
HAPI Web 中的 Codex session 现在可以发现本地 Codex skills,并通过可搜索的选择器插入显式
$skill-name调用文本。HAPI 仍然只做输入辅助:只读取有限的 skill metadata,不展开完整SKILL.md,不执行 skill 脚本,也不会自动发送 composer 内容。主要改动
GET /api/sessions/:id/skills,仅 Codex session 返回 skills,非 Codex session 返回空数组。.codex-plugin/plugin.json扫描 Codex plugin cache,并返回类似compound-engineering:ce-plan的可调用名称。$的小型 autocomplete 替换为更大的可搜索 picker,支持键盘和鼠标选择。验证
bun run typecheckbun run test:hubbun run test:webbun run build:web$会打开 skill picker,并只插入$compound-engineering:ce-plan;Claude session 输入$不会打开 picker。