Describe the bug
Uninstalling a user-scope package (apm install -g → apm uninstall -g) removes
every other primitive it deployed but leaves its Copilot hook files behind in
~/.copilot/hooks/. The orphaned file is live wiring: Copilot CLI keeps firing a
hook belonging to a package that is gone from apm.yml, apm_modules/, and the
lockfile. Reinstalling never clears it either — the path is simply never a
deletion candidate.
Copilot is the only harness affected, and only at user scope. Uninstall's
file-level removal routes each lockfile deployed_files path into a per-primitive
bucket via
BaseIntegrator.partition_managed_files,
which walks a segment trie built from every non-skills/hooks primitive prefix
and falls back to the skills/hooks prefix tuples only when the trie yields no
match:
if last_bucket is not None:
buckets[last_bucket].add(p)
continue
# Fall back to cross-target buckets
if p.startswith(skill_tuple):
buckets["skills"].add(p)
elif p.startswith(hook_tuple):
buckets["hooks"].add(p)
At user scope the Copilot target's
user_primitive_overrides
maps instructions to PrimitiveMapping("", ".md", "copilot_user_instructions") —
an empty subdir, so its prefix is the bare target root .copilot/. That prefix
becomes a catch-all trie node: .copilot/hooks/<pkg>-<hook>.json matches at the
first segment, last_bucket is set to instructions_copilot, and the continue
skips the hook fallback. The hook path is handed to
InstructionIntegrator.sync_for_target,
whose copilot_user_instructions guard narrows the prefix to
.copilot/copilot-instructions.md and discards it. No integrator ever deletes the
file, and HookIntegrator.sync_integration never sees it in managed_files.
Nothing shadows .copilot/prompts/ or .copilot/agents/ — they own deeper trie
nodes and win by longest prefix. Hooks lose because they are routed by the
fallback tuples rather than by the trie. At project scope Copilot hooks land in
.github/hooks/, outside the .copilot/ root, so the shadowing never applies and
removal works. The other harnesses escape for a different reason: their hooks are
merged into a shared config (.claude/settings.json, .cursor/hooks.json, …) and
cleaned by _apm_source marker rather than by managed_files.
The symptom resembles #2245 / #2250, but those cover the merged-hook JSON path;
this is the per-file deployment path and a different root cause. Same target and
scope as #2232, also a different cause.
To Reproduce
Steps to reproduce the behavior:
cd uninstall-copilot-user-hook-orphan
make user — installs a package holding one hook and one prompt to user scope
(--target copilot) under an isolated fake $HOME, then uninstalls it
- See the prompt is removed and reported (
Cleaned up 1 integrated prompts),
while .copilot/hooks/hookpkg-notify.json survives and no hook cleanup is
reported at all
make matrix — repeats install + uninstall at user scope for every hook-capable
harness; only copilot reports an orphan
make project — installs the same package at project scope across all those
harnesses; every hook is removed
Expected behavior
apm uninstall -g removes the hook files it deployed to ~/.copilot/hooks/, the
same way it removes the package's prompts, and the same way project-scope
uninstall removes Copilot hooks from .github/hooks/. After uninstall no
harness still fires a hook from the removed package.
Environment (please complete the following information):
- OS: Linux (Arch Linux)
- Python Version: 3.14.6
- APM Version: 0.28.0 (e041462)
Logs
make user — prompt cleaned, hook orphaned:
[+] ./hookpkg (local)
|-- 1 prompts integrated -> .copilot/prompts/
|-- 1 hook(s) integrated -> .copilot/hooks/
| postToolUse: runs echo hookpkg-ran (notify.json)
[*] Installed 1 APM dependency in 1.9s.
--- deployed under ~/.copilot/ ---
./.copilot/hooks/hookpkg-notify.json
./.copilot/prompts/greet.prompt.md
[i] Uninstalling from user scope (~/.apm/)
[>] Uninstalling 1 package(s)...
[+] _local/hookpkg - found in apm.yml
[i] Removed _local/hookpkg from dependencies.apm in apm.yml
[i] Removed _local/hookpkg from apm_modules/
[+] Cleaned up 1 integrated prompts
[*] Uninstall complete: Removed 1 package(s) from apm.yml, Removed 1 package(s) from apm_modules/
--- after uninstall (prompt gone, hook orphaned) ---
./.copilot/hooks/hookpkg-notify.json
make matrix — user scope, every hook-capable harness:
claude: clean
codex: clean
copilot: ORPHANED ./.copilot/hooks/hookpkg-notify.json
cursor: clean
gemini: clean
windsurf: clean
make project — same package, project scope, all six harnesses:
[+] Cleaned up 1 integrated prompts
[+] Cleaned up 4 integrated commands
[+] Cleaned up 6 integrated hooks
[*] Uninstall complete: Removed 1 package(s) from apm.yml, Removed 1 package(s) from apm_modules/
--- after uninstall (no file still carries the hook command) ---
Additional context
Minimal reproduction: https://github.com/sproott/apm-bugs
(make user / make matrix / make project in
uninstall-copilot-user-hook-orphan/).
Describe the bug
Uninstalling a user-scope package (
apm install -g→apm uninstall -g) removesevery other primitive it deployed but leaves its Copilot hook files behind in
~/.copilot/hooks/. The orphaned file is live wiring: Copilot CLI keeps firing ahook belonging to a package that is gone from
apm.yml,apm_modules/, and thelockfile. Reinstalling never clears it either — the path is simply never a
deletion candidate.
Copilot is the only harness affected, and only at user scope. Uninstall's
file-level removal routes each lockfile
deployed_filespath into a per-primitivebucket via
BaseIntegrator.partition_managed_files,which walks a segment trie built from every non-
skills/hooksprimitive prefixand falls back to the
skills/hooksprefix tuples only when the trie yields nomatch:
At user scope the Copilot target's
user_primitive_overridesmaps instructions to
PrimitiveMapping("", ".md", "copilot_user_instructions")—an empty
subdir, so its prefix is the bare target root.copilot/. That prefixbecomes a catch-all trie node:
.copilot/hooks/<pkg>-<hook>.jsonmatches at thefirst segment,
last_bucketis set toinstructions_copilot, and thecontinueskips the hook fallback. The hook path is handed to
InstructionIntegrator.sync_for_target,whose
copilot_user_instructionsguard narrows the prefix to.copilot/copilot-instructions.mdand discards it. No integrator ever deletes thefile, and
HookIntegrator.sync_integrationnever sees it inmanaged_files.Nothing shadows
.copilot/prompts/or.copilot/agents/— they own deeper trienodes and win by longest prefix. Hooks lose because they are routed by the
fallback tuples rather than by the trie. At project scope Copilot hooks land in
.github/hooks/, outside the.copilot/root, so the shadowing never applies andremoval works. The other harnesses escape for a different reason: their hooks are
merged into a shared config (
.claude/settings.json,.cursor/hooks.json, …) andcleaned by
_apm_sourcemarker rather than bymanaged_files.The symptom resembles #2245 / #2250, but those cover the merged-hook JSON path;
this is the per-file deployment path and a different root cause. Same target and
scope as #2232, also a different cause.
To Reproduce
Steps to reproduce the behavior:
cd uninstall-copilot-user-hook-orphanmake user— installs a package holding one hook and one prompt to user scope(
--target copilot) under an isolated fake$HOME, then uninstalls itCleaned up 1 integrated prompts),while
.copilot/hooks/hookpkg-notify.jsonsurvives and no hook cleanup isreported at all
make matrix— repeats install + uninstall at user scope for every hook-capableharness; only
copilotreports an orphanmake project— installs the same package at project scope across all thoseharnesses; every hook is removed
Expected behavior
apm uninstall -gremoves the hook files it deployed to~/.copilot/hooks/, thesame way it removes the package's prompts, and the same way project-scope
uninstall removes Copilot hooks from
.github/hooks/. After uninstall noharness still fires a hook from the removed package.
Environment (please complete the following information):
Logs
make user— prompt cleaned, hook orphaned:make matrix— user scope, every hook-capable harness:make project— same package, project scope, all six harnesses:Additional context
Minimal reproduction: https://github.com/sproott/apm-bugs
(
make user/make matrix/make projectinuninstall-copilot-user-hook-orphan/).