OpenHands / OpenHands/software-agent-sdk
[Bug]: Installed skill resources leak into permanent context even when disabled
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Is there an existing issue for the same bug?
- I have searched existing issues and this is not a duplicate.
Supersedes #5021, which was filed from an automation account by mistake and will be closed.
Bug Description
Managed installed skills leak their supporting markdown (commands/, references/) into always-active REPO_CONTEXT through generic user-skill discovery. Disabling the installed skill removes its callable entry but does not stop the supporting documents from loading.
load_user_skills() scans the parent skills directory and then calls the managed installed-skills loader. find_skill_md_directories() only recognizes direct-child package roots, while find_regular_md_files() discovers loose markdown recursively. The extra installed/ level means supporting documents escape the package exclusion, and because they carry no triggers frontmatter they are classified as permanent repository context.
This defeats the progressive disclosure the docs promise: "The agent reads referenced files from scripts/, references/, or assets/ only when needed" (docs.openhands.dev/overview/skills).
Expected Behavior
enabled: ['example']
disabled: []
A packaged skill is advertised once as a callable skill. Its instructions load when invoked; its supporting resources load only when the skill reads them.
Actual Behavior
Save the snippet under Minimal Code Sample as repro.py in a checkout of this repository and run it:
make build
uv run python repro.py
Output:
enabled: ['example', 'installed/example/references/notes']
disabled: ['installed/example/references/notes']
Every non-README/non-SKILL markdown file in the package becomes a standalone always-on skill, named by its path relative to the scan root. Disabling the package does not remove them.
The same run against a checkout with the managed installation subtree excluded from generic discovery prints the expected two lines, so the reproduction isolates this behavior rather than a packaging or configuration difference.
Regression coverage lives in tests/sdk/skills/test_load_user_skills.py:
uv run pytest tests/sdk/skills tests/sdk/context
Steps to Reproduce
- Install a skill package that has a
references/(orcommands/) directory into the managed installed-skills directory. - Call
load_user_skills()and observe the extrainstalled/<pkg>/references/<file>entries. - Call
disable_skill("<pkg>")and callload_user_skills()again. - The callable skill disappears, the supporting documents remain.
Acceptance Criteria
-
load_user_skills()returns only the callable skill for an enabled installed package. -
load_user_skills()returns nothing for that package when it is disabled. - Supporting resource files stay on disk and remain readable when the skill is invoked.
- Recursive discovery of loose legacy markdown outside the managed directory is unchanged.
- Direct-child AgentSkills packages and user-over-installed precedence are unchanged.
Installation Method
Ships inside ghcr.io/openhands/agent-canvas:1.18.0; also reproduced from a source checkout via make build and uv run.
If you selected "Other", please specify
No response
SDK Version
1.46.0 (observed in production). Reproduced against main at c37007429be8b4465a83487dc1fd0914df0ea734; the relevant logic is unchanged in v1.47.0.
Version Confirmation
- I have confirmed this bug exists on the LATEST version of OpenHands SDK
Python Version
3.13.15
Model Name (if applicable)
N/A — this is a context-assembly bug, model independent.
Operating System
Linux
Logs and Error Messages
No error is raised; the failure is silent. Observed on a live deployment, where one installed package (iterate) contributed six always-on entries:
installed/iterate/commands/babysit
installed/iterate/commands/iterate
installed/iterate/commands/verify
installed/iterate/references/heuristics
installed/iterate/references/verification
installed/iterate/references/windows
The three command wrappers are near-identical one-line shims, and the reference documents are injected in full on every conversation whether or not the skill is invoked.
Minimal Code Sample
from pathlib import Path
from tempfile import TemporaryDirectory
from openhands.sdk.skills import (
skill, installed, install_skill, disable_skill, load_user_skills,
)
old_dirs = skill.USER_SKILLS_DIRS
old_installed = installed.DEFAULT_INSTALLED_SKILLS_DIR
try:
with TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "example"
source.mkdir()
(source / "SKILL.md").write_text(
"---\nname: example\ndescription: Example\n---\nInstructions"
)
(source / "references").mkdir()
(source / "references" / "notes.md").write_text("Resource instructions")
skill.USER_SKILLS_DIRS = [root / "skills"]
installed.DEFAULT_INSTALLED_SKILLS_DIR = root / "skills" / "installed"
install_skill(str(source))
print("enabled:", sorted(s.name for s in load_user_skills()))
disable_skill("example")
print("disabled:", sorted(s.name for s in load_user_skills()))
finally:
skill.USER_SKILLS_DIRS = old_dirs
installed.DEFAULT_INSTALLED_SKILLS_DIR = old_installed
Screenshots and Additional Context
Suggested fix: exclude the managed installation subtree from generic discovery and leave enable/disable decisions to the installed-skills loader, preserving ordinary recursive legacy markdown discovery, direct-child AgentSkills resources, and user-over-installed precedence.
Scope note: the leak grows with package size and affects every installed package that ships supporting files, so the wasted context is paid on every conversation.
Related: #1981 (closed public-skills reference leakage) and #4495 (plugin discovery refactor, which does not cover the managed user-installation overlap).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with load_user_skills(), find_skill_md_directories(), find_regular_md_files(), and the managed installed-skills loader. Run the reproduction with make build and uv run python repro.py, then inspect tests/sdk/skills/test_load_user_skills.py and tests/sdk/context. Done means enabled packages expose only their callable skill, disabled packages expose nothing, and the listed discovery behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- testing-qa, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100