Skip to content

fix: prevent skill-active-state collision between OMC and project custom skills#1741

Merged
Yeachan-Heo merged 1 commit intoYeachan-Heo:devfrom
dlwnstjr0310:fix/skill-active-state-custom-skill-collision
Mar 18, 2026
Merged

fix: prevent skill-active-state collision between OMC and project custom skills#1741
Yeachan-Heo merged 1 commit intoYeachan-Heo:devfrom
dlwnstjr0310:fix/skill-active-state-custom-skill-collision

Conversation

@dlwnstjr0310
Copy link
Contributor

Problem

When a project defines a custom skill with the same bare name as an OMC built-in skill (e.g., .claude/skills/plan/ vs oh-my-claudecode:plan), the pre-tool hook applies OMC's SKILL_PROTECTION map to the custom skill because extractSkillName() / getInvokedSkillName() strip the namespace prefix before lookup. This writes skill-active-state.json with active: true, causing the stop hook to block session termination with reinforcement messages — even after the custom skill has completed.

Reproduction

  1. Create a project skill at .claude/skills/plan/ (or any name in SKILL_PROTECTION_MAP)
  2. Invoke it via /plan
  3. The Skill tool sends skill: "plan" (no OMC prefix)
  4. getSkillProtectionLevel("plan") returns 'medium' → state file written
  5. Stop hook blocks with [SKILL ACTIVE: plan] reinforcement messages

This is a follow-up to #1581 — that fix changed the default for unregistered skills to 'none', but skills like plan, review, tdd are explicitly registered in SKILL_PROTECTION_MAP with non-none levels, so the collision persists.

Fix

Pass the raw (un-normalized) skill name through the call chain so getSkillProtection() can distinguish OMC built-in skills (prefixed with oh-my-claudecode:) from project custom or other plugin skills. Non-OMC-prefixed skills now correctly default to 'none' protection regardless of name collisions.

Changes

File Change
src/hooks/skill-state/index.ts Add optional rawSkillName param to getSkillProtection() and writeSkillActiveState()
src/hooks/bridge.ts Add getRawSkillName() helper; pass raw name to writeSkillActiveState()
scripts/pre-tool-enforcer.mjs Same fix for the CJS runtime hook path
src/hooks/skill-state/__tests__/skill-state.test.ts 7 new test cases for custom skill distinction

Behavior matrix

Invocation rawSkillName Protection
Skill(skill: "oh-my-claudecode:plan") "oh-my-claudecode:plan" medium
Skill(skill: "plan") (project custom) "plan" none
Skill(skill: "ouroboros:plan") "ouroboros:plan" none
Skill(skill: "plan") (no rawSkillName, legacy) undefined medium (backward compat)

Test plan

  • skill-state.test.ts: 44 tests passed (7 new)
  • skill-state-stop.test.ts: 8 tests passed (integration)
  • Backward compatible — rawSkillName is optional; omitting it preserves existing behavior

Retargeted from #1739 (was targeting main).

…tom skills

When a project defines a custom skill with the same name as an OMC built-in
skill (e.g., `.claude/skills/plan/`), the pre-tool hook incorrectly applied
OMC's SKILL_PROTECTION map to the custom skill, writing skill-active-state.json
with `active: true`. This caused the stop hook to block session termination
with reinforcement messages even after the custom skill completed.

The fix passes the raw (un-normalized) skill name through the call chain so
`getSkillProtection()` can distinguish OMC skills (prefixed with
`oh-my-claudecode:`) from project custom or other plugin skills. Non-OMC
skills now correctly default to `'none'` protection.

Affected files:
- src/hooks/skill-state/index.ts: Add rawSkillName param to getSkillProtection/writeSkillActiveState
- src/hooks/bridge.ts: Extract and pass raw skill name via getRawSkillName()
- scripts/pre-tool-enforcer.mjs: Same fix for the CJS runtime hook

Ref: Yeachan-Heo#1581
@Yeachan-Heo Yeachan-Heo merged commit 9bf9729 into Yeachan-Heo:dev Mar 18, 2026
14 checks passed
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fda58b843e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +139 to 146
export function getSkillProtection(skillName: string, rawSkillName?: string): SkillProtectionLevel {
// When rawSkillName is provided, only apply protection to OMC-prefixed skills.
// Non-prefixed skills are project custom skills or other plugins — no protection.
if (rawSkillName != null && !rawSkillName.toLowerCase().startsWith('oh-my-claudecode:')) {
return 'none';
}
const normalized = skillName.toLowerCase().replace(/^oh-my-claudecode:/, '');
return SKILL_PROTECTION[normalized] ?? 'none';

Choose a reason for hiding this comment

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

P2 Badge Regenerate shipped hook bundles for rawSkillName support

This source change never reaches the package entrypoints that users actually run. package.json ships dist/index.js and bridge/cli.cjs, but in this commit the checked-in generated files still contain the old getSkillProtection(skillName) / writeSkillActiveState(directory, skillName, sessionId) logic and bridge/cli.cjs still calls the 3-argument form, so imported processHook() consumers and the bundled CLI continue to reproduce the plan/review collision even though src/hooks/skill-state/index.ts is fixed here.

Useful? React with 👍 / 👎.

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