A Sync Script Should Delete Only What It Wrote
Until version 0.2.2, every agentsync sync began by emptying the folders it was about to write into. For skills, that line was rm -rf "$TARGET/skills/"*.
agentsync is my small open-source generator for agent configs. I'd seen prompts drift across tools when each tool kept its own hand-written copy, so agentsync keeps one source. You edit agents/, run a sync script, and it writes .claude/, .codex/, .opencode/, .agents/skills/, and optionally .github/ for Copilot. The README it drops into agents/ puts it bluntly: never edit .claude/ directly, because it's regenerated on every sync.
Emptying a folder assumes you own it
In the old Claude Code script, the skills step was four lines. Here they are, next to what replaced them:
-rm -rf "$TARGET/skills/"*
-if [[ -d "$SKILLS_SRC" ]]; then
- cp -r "$SKILLS_SRC/"* "$TARGET/skills/"
-fi
+sync_skill_dirs_verbatim "$SKILLS_SRC" "$TARGET/skills"
Wipe, then copy. The output can't go stale, and a skill you delete from the source is gone from the output after the next run. The Codex, OpenCode, and Copilot scripts had the same shape.
That's only correct if agentsync is the one thing writing there, and it isn't. .claude/skills/ is where Claude Code looks for skills. Your own skills live there too, and so does whatever another tool's generator produces. A blanket delete treats all of it as stale agentsync output.
Write down what you wrote
The fix is a hidden file, .agentsync-manifest, in each skills, agents, and rules folder agentsync writes to. It holds one entry per line, one for each skill folder, agent file, or rule file agentsync put there. On the next sync, the prune reads that list and removes only entries that are on it and have since left the source:
if [[ -f "$manifest" ]]; then
local prev name keep
while IFS= read -r prev; do
[[ -n "$prev" ]] || continue
keep=0
for name in ${owned[@]+"${owned[@]}"}; do
[[ "$name" == "$prev" ]] && { keep=1; break; }
done
[[ $keep -eq 0 ]] && rm -rf "${target:?}/$prev"
done < "$manifest"
fi
Then it rewrites the manifest with the new set and copies the owned entries in. Anything that was never on the list stays where it is, unless the source has one with the same name. (The odd ${owned[@]+...} expansion is for bash 3.2, which treats an empty array as unbound under set -u.)
This blog is a working example. Its writing skills live in a top-level skills/ folder and are symlinked into .claude/skills/. The manifest there lists three entries: draft-post, portfolio-ground-truth, and shape-post. The three symlinks aren't on it, and they were still there after the last sync.
.gitignore gets the same treatment. agentsync writes its patterns between # >>> agentsync >>> and # <<< agentsync <<<, and later runs replace only that block. The rest of the file is yours.
Good manners only work one way
The manifest makes agentsync a polite neighbor. It does nothing about rude ones. If another tool's generator still empties the folder before writing, its next run deletes agentsync's skills. agentsync can't prevent that. It can notice, though not from the sync script itself. The agentsync skill checks at setup and again during an audit, looking for files that carry another tool's "generated by" or "do not edit" banner. Then it reports the shared folder and the risk, and you decide whether to keep sharing it.