title: "Skill server feed: no prune/delete handling for removed skills"
labels: ["bug", "skills"]
Problem
When a skill is deleted (or renamed) on a private skill server, the Netclaw daemon does not detect or clean up the stale skill on disk. The ServerFeedSyncService only iterates forward through the server index, it downloads/updates skills that exist, but never prunes skills that were removed.
What actually happens
- Local disk persists - the
~/.netclaw/skills/server-feed:{feed-name}/{skill-name}/ directory remains after the skill is deleted from the server
- Sync state remains stale - the
SkillSyncState.Skills dictionary still contains the removed skill (version + sha256)
- Duplicate on rename - renaming a skill creates both the old (stale) and new (fresh) versions on disk, both registered in
SkillRegistry
- Agents are unaware -
skill_load continues to work on deleted skills since they live on disk locally
Affected components
| Layer |
Path |
Responsibility |
SkillServerClient |
netclaw-dev/skill-server (NuGet package Netclaw.SkillClient) |
HTTP transport only, no lifecycle logic |
ServerFeedSyncService |
src/Netclaw.Daemon/Services/ServerFeedSkillSyncService.cs |
Orchestrator, should prune |
SkillSyncState |
src/Netclaw.Configuration/Feeds/SkillSyncState.cs |
State tracking, never pruned |
SkillRegistry |
In-memory, rebuilt via RescanAndUpdateIndex() |
Rebuilt from disk, so stale entries persist |
Where the gap is
In ServerFeedSyncService.SyncAllFeedsAsync(), after the SyncFeedAsync loop completes, there is no post-pass to compare local state against the server index. The current flow only handles two cases per skill: skip if version matches, or download+verify if new/updated. There is no handling for a skill that exists locally but is no longer in the server index.
Proposed fix
Add a post-scan step after the main index iteration:
- Prune stale sync state entries (
syncState.Skills.Keys.Except(index.Skills))
- Prune stale on-disk directories (
Directory.GetDirectories(feedDir) not in index)
Or alternatively, compare the union of syncState.Skills.Keys plus on-disk directories as a single set operation.
Impact
- Low risk: pruning only removes skills not in the current server index
- Safe: atomic directory replacement pattern already used by
ReplaceSkillDirectoryAsync
- No protocol change needed: the RFC index already defines what exists; what does not is a local pruning decision
Related
SkillServerClient is a separate repo (netclaw-dev/skill-server) and should stay as a pure HTTP client
- This is purely an orchestration gap in
ServerFeedSyncService
title: "Skill server feed: no prune/delete handling for removed skills"
labels: ["bug", "skills"]
Problem
When a skill is deleted (or renamed) on a private skill server, the Netclaw daemon does not detect or clean up the stale skill on disk. The
ServerFeedSyncServiceonly iterates forward through the server index, it downloads/updates skills that exist, but never prunes skills that were removed.What actually happens
~/.netclaw/skills/server-feed:{feed-name}/{skill-name}/directory remains after the skill is deleted from the serverSkillSyncState.Skillsdictionary still contains the removed skill (version + sha256)SkillRegistryskill_loadcontinues to work on deleted skills since they live on disk locallyAffected components
SkillServerClientnetclaw-dev/skill-server(NuGet packageNetclaw.SkillClient)ServerFeedSyncServicesrc/Netclaw.Daemon/Services/ServerFeedSkillSyncService.csSkillSyncStatesrc/Netclaw.Configuration/Feeds/SkillSyncState.csSkillRegistryRescanAndUpdateIndex()Where the gap is
In
ServerFeedSyncService.SyncAllFeedsAsync(), after theSyncFeedAsyncloop completes, there is no post-pass to compare local state against the server index. The current flow only handles two cases per skill: skip if version matches, or download+verify if new/updated. There is no handling for a skill that exists locally but is no longer in the server index.Proposed fix
Add a post-scan step after the main index iteration:
syncState.Skills.Keys.Except(index.Skills))Directory.GetDirectories(feedDir)not in index)Or alternatively, compare the union of
syncState.Skills.Keysplus on-disk directories as a single set operation.Impact
ReplaceSkillDirectoryAsyncRelated
SkillServerClientis a separate repo (netclaw-dev/skill-server) and should stay as a pure HTTP clientServerFeedSyncService