Repository navigation
[bug] --no-skills is bypassed by plugin-provided skills via extendResources #216
Description
Activity
I tried to falsify this and could not. The bypass is real and reachable in production — I confirmed it end-to-end rather than only at the unit level. But the second half of my suggested fix is wrong and would break intended behaviour. Correcting it.
The bug stands, and the evidence is stronger than I gave
The unit reproduction in the original report is correct (
[]→["evil-skill"]). I additionally wired up the production path the report did not cover — same shape asapps/cli/src/main.ts:1168-1181, i.e.noSkills: truewith the step extension passed toDefaultResourceLoaderas an inlineextensionFactory, a real plugin directory, and a realbindExtensionscall:E2E loader after reload(): [] E2E extensions loaded: [ '<inline:Step>' ] E2E hasHandlers(resources_discover): true E2E AFTER bindExtensions skills: ["evil-skill"]With a read tool present, the injected body reaches the system prompt:
PROMPT has <available_skills>: true PROMPT has ATTACKER CONTROLLED: trueReachability in the real CLI is unconditional, and I checked each gate:
apps/cli/src/main.ts:185passescreateStepExtensionFactories(...)with nonoSkillscondition;apps/cli/src/bootstrap/extensions.ts:41-54is a static array.features/step.ts:116unconditionally registerscreateStepPluginResourcesExtension, which handlesresources_discover(step/plugins.ts:1187).- Inline factories are always loaded (
resource-loader.ts:564-571,:584,:614);noExtensionsdoes not gate them either. discoverStepPluginResourcePaths(step/plugins.ts:726) readsdefaultStepPluginsDir— the global root is always read, only the project root is trust-gated (:735-741).
Correction: do NOT change the guard in
updateSkillsFromPathsMy report suggested "make
updateSkillsFromPathstestthis.noSkillsdirectly so it cannot be re-entered". That is wrong and would regress intended behaviour.test/resource-loader-no-skills.test.ts:64-84explicitly pins that undernoSkills: true, explicitly supplied skill directories still load:// still discovers explicitly supplied skill directories ... additionalSkillPaths: [explicitDir] ... expect(loader.getSkills().skills.map((s) => s.name)).toEqual(["explicit-skill"]);
Changing the guard to
if (this.noSkills)turns "disable discovery" into "disable everything" and that test fails immediately.The intent is visible in the CLI help next to the sibling flag:
--no-extensions, -ne Disable extension discovery (explicit -e paths still work) --no-skills, -ns Disable skills discovery and loadingSo the existing guard — "respect
noSkillsonly when there are no explicit paths" — is deliberate, and plugin paths are simply not being classified as explicit. My reproduction could not distinguish the two because it fed plugin paths through the same channel asadditionalSkillPaths; that ambiguity is what hid the conflict.The wording "discovery and loading" in the
--no-skillshelp is admittedly broader than "discovery", so there is a fair argument that the help text is the thing to tighten.Corrected fix suggestion
Apply the check only where paths are admitted, in
extendResources— drop the corresponding list whennoSkills/noPromptTemplates/noThemesis set. Leave theupdateSkillsFromPathsguard alone.A regression test should assert both halves, since either alone is satisfied by today's broken state:
- under
noSkills: true, plugin-contributed skill paths are dropped, and - under
noSkills: true,additionalSkillPathsstill load.
Attribution, re-verified
git log -L :extendResources:packages/coding-agent/src/core/resource-loader.tsproduces no output — the method has not been modified since the initial import4fdb781. Inbda152e^(pre-#204) the stringpi.on("resources_discover"has zero hits acrosspackages/andapps/: the channel existed but nothing reported through it, soskillPaths.lengthstayed 0 and the existing guard held by accident. #204 is what made the pre-existing gap reachable, which is how the original report characterised it — that part is accurate.Reopening — I closed this too early.
I read the maintainers'
SECURITY.mdscoping and treated it as covering this report, but it only carves out the security surface of installed extensions. The functional side is not excluded, and the gap is still present onmain(519e4de).I have also corrected the "Suggested fix" section above — its second half was wrong, and the comment I left earlier spells out why. The short version: the
updateSkillsFromPathsguard is deliberate, and the check belongs inextendResourcesonly.If this is considered intended behaviour, feel free to close it again.
[bug]
--no-skillsis bypassed by plugin-provided skills viaextendResourcesSummary
--no-skillsis documented as "Disable skills discovery and loading". The guard inupdateSkillsFromPathsis:It short-circuits only when the path list is empty. On startup,
AgentSession.extendResourcesFromExtensionscallsresourceLoader.extendResources(...)with the paths reported by extensions.extendResourcesmerges those paths intolastSkillPathswithout consultingthis.noSkills, so the second call intoupdateSkillsFromPathssees a non-empty list and loads for real.A plugin under
~/.stepcode/plugins/that declaresskillstherefore has itsSKILL.mdcontent injected into the system prompt even when the user passed--no-skills. The same pattern applies tonoPromptTemplatesandnoThemes:extendResourceschecks none of the three flags.This is a prompt-injection surface as well as a bypass: the content that gets injected comes from a plugin directory, and
SKILL.mdbodies are rendered into the system prompt byformatSkillsForPrompt.Impact
A user's explicit "do not load skills" intent is silently ignored for plugin-provided skills, commands and themes. Combined with the marketplace install path, this widens what a third-party plugin can put in front of the model.
Reproduction
Real run against
mainat519e4dewith the repository's own runner, using the #204 discovery function so the path is the production one.Observed output:
The baseline is correctly empty; the extension path is what loads it.
Root cause
packages/coding-agent/src/core/resource-loader.ts:678— the guard testsskillPaths.length === 0instead ofthis.noSkillsitself.packages/coding-agent/src/core/resource-loader.ts:347,362,369,377—extendResourcesmerges and recomputes without checkingnoSkills,noPromptTemplatesornoThemes.packages/coding-agent/src/core/agent-session.ts:2620-2635—extendResourcesFromExtensionsis called unconditionally onsession_start/ reload, and only bails when all three lists are empty.Attribution and reachability
extendResourceshas never checked these flags — it is present from the initial import commit4fdb781. What changed is reachability: before #204 the plugin channel did not report skill/prompt paths, soskillPaths.lengthstayed 0 and the existing guard happened to hold. #204 ("fix(plugins): load plugin skills and commands") made plugin resources reach this path, which turns the pre-existing gap into a live bypass. Reporting against currentmainsince that is where it is observable.Suggested fix
Check the flags at the point where paths are admitted, not only at the point where they are loaded — in
extendResources, drop the corresponding list whennoSkills/noPromptTemplates/noThemesis set. That is a three-line change.The
updateSkillsFromPathsguard should stay as it is.test/resource-loader-no-skills.test.ts:64-84pins that explicitly supplied skill directories still load undernoSkills: true, so testingthis.noSkillsthere directly would turn "disable discovery" into "disable everything" and fail that test. A regression test for this should assert both halves: plugin-contributed paths are dropped undernoSkills, andadditionalSkillPathsstill load.There is no test covering this combination today:
test/resource-loader-no-skills.test.tscoversadditionalSkillPathsundernoSkills(the case that is meant to keep loading) and the package-skill case, but never callsextendResources. A test doing exactly the sequence above would pin it.Verification performed
stepfun-ai/Step-Code,mainat519e4de(includes fix(plugins): load plugin skills and commands, and fix MCP discovery #204).vitest(4.1.9) on Node 24.19.0, Windows.%TEMP%; no repository files were modified.extendResourceschange applied on top of519e4de: the package test suite shows no new failures andtsgo --noEmitis clean.