Skip to content

Commit 0ef983e

Browse files
author
Fernando Pinho
committed
add skillctl remove and release v0.2.0
Until now there was no way to take a skill back out of a project: an operator could install, push, and pull, but removing a skill meant hand-deleting the folder and editing `.skills.toml` by hand. `remove` closes that gap. It lists every removable skill — installed via skillctl, created locally, or an orphaned manifest entry whose folder is already gone — distinguishes the three kinds in the picker, and deletes the chosen folders while dropping their `.skills.toml` entries. It is project-only and never touches the library or git. Minor bump rather than patch because it adds a command. The feature went through the standard pre-release audit pass (FS-safety, untrusted-input, logic); the deletion path re-checks for a symlink immediately before unlinking so a swapped folder can't redirect the recursive delete outside the project, and `--skill` fails closed on unknown or ambiguous names.
1 parent 031d1ab commit 0ef983e

9 files changed

Lines changed: 481 additions & 3 deletions

File tree

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Core flows the CLI must support:
2323
- `push` — project → library (propagate local edits)
2424
- `fork` — project → library as a *new* skill (when local edits should not overwrite the original)
2525
- `detect` — project → library (find new skills created locally and offer to add them)
26+
- `remove` — project-only (delete installed/local skill folders and drop their `.skills.toml` entries; never touches the library or git)
2627

2728
## Conventions
2829
- Prefer typed errors (`thiserror` / `anyhow` at the binary edge) over `unwrap`/`expect` outside tests.

.claude/skills/skillctl-usage/SKILL.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Per-command top-level shape:
4040

4141
// detect
4242
{"command":"detect","target":"…|null","results":[{"name":"","status":"added|skipped","library_path":"","local_path":"","source_sha":""}],"commit":{"sha":"","message":""}|null,"summary":{"added":N,"skipped":N}}
43+
44+
// remove
45+
{"command":"remove","results":[{"name":"","status":"removed|failed","path":"","removed_folder":true|false,"removed_entry":true|false,"reason":""}],"summary":{"removed":N,"failed":N}}
4346
```
4447

4548
Stable rules:
@@ -190,6 +193,26 @@ skillctl detect --tag <tag> [--tag <tag> …] [--all-tags] --target <library-pat
190193

191194
`skillctl detect` walks the current directory for `SKILL.md` files, drops anything already declared in `.skills.toml`, copies the leftovers into the library cache under `<target>/<skill-folder-name>`, single-commits with a `add skill(s): …` message, pushes, and appends the new entries to `.skills.toml`.
192195

196+
### `skillctl remove` — remove skills from the current project
197+
198+
```sh
199+
skillctl remove --skill <name> [--skill <name> …]
200+
skillctl remove --all
201+
```
202+
203+
| Flag | Purpose | Required in non-interactive |
204+
|---|---|---|
205+
| `--skill <name>` | Remove a specific skill by name (repeatable). Mutually exclusive with `--all`. Errors if the name is unknown or ambiguous (two skills share it). | Yes, unless `--all` |
206+
| `--all` | Remove every removable skill found in the project. Mutually exclusive with `--skill`. | Yes, unless `--skill` |
207+
208+
`skillctl remove` is **project-only** — it never touches the library or git. It walks the current directory for skill folders (respecting `.gitignore`, skipping `node_modules`/`target`) and cross-references `.skills.toml`, presenting three kinds of removable skill:
209+
210+
- **installed via skillctl** — folder present *and* tracked in `.skills.toml`. Removing it deletes the folder and drops the entry.
211+
- **created locally, not tracked** — folder present but absent from `.skills.toml`. Removing it deletes the folder only.
212+
- **orphan** — a `.skills.toml` entry whose folder is already gone. Removing it drops the stale entry only (nothing to delete on disk).
213+
214+
In each `results[]` item, `removed_folder` and `removed_entry` report which of the two actions actually happened. `.skills.toml` is only rewritten when at least one tracked entry is dropped. In an interactive TTY, a confirmation prompt is shown before anything is deleted; in non-interactive/`--json` mode the explicit `--skill`/`--all` flags are the authorisation. A symlinked destination is never followed — it is treated as "no folder on disk" so removal can only ever drop its manifest entry, never delete through the link.
215+
193216
## Skill identity
194217

195218
A "skill" is any folder containing a file literally named `SKILL.md`. The skill's `name` comes from the YAML frontmatter `name:` field at the top of `SKILL.md`; if absent, the folder name is used. All `--skill <name>` flags match against this resolved name.
@@ -212,7 +235,7 @@ Errors always go to stderr regardless of mode.
212235
213236
## Interactive prompt (multi-select with live filter)
214237
215-
When a multi-select prompt opens (in `add`, `push`, `pull`, `detect` without flags or `--all` and a TTY), the prompt has a live filter:
238+
When a multi-select prompt opens (in `add`, `push`, `pull`, `detect`, `remove` without flags or `--all` and a TTY), the prompt has a live filter:
216239
217240
- **Type any character** — appends to the filter; the list filters in real time on the skill name (substring, case-insensitive).
218241
- **Backspace** — edits the filter.
@@ -312,6 +335,18 @@ skillctl detect --all --target skills
312335
skillctl push --skill review --skill security-review --message "polish: tighter reviewer prompts"
313336
```
314337
338+
### Remove specific skills from a project
339+
340+
```sh
341+
skillctl remove --skill claude-api --skill review
342+
```
343+
344+
### Remove every skill from a project (folders + manifest entries)
345+
346+
```sh
347+
skillctl remove --all
348+
```
349+
315350
## Failure modes worth checking before invoking
316351

317352
1. **No library configured.** Calls fail with `no library configured — run skillctl init <github-url> first`. Run `skillctl init` (or check for an existing library URL via inspecting `~/.config/skills-cli/config.toml` on Linux or the equivalent under `~/Library/Application Support/dev.umanio-agency.skills-cli/` on macOS).

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66

77
## [Unreleased]
88

9+
## [0.2.0] - 2026-05-27
10+
11+
### Added
12+
13+
- **`skillctl remove`** — remove skills from the current project. Lists every removable skill (installed via skillctl, created locally, or an orphaned `.skills.toml` entry whose folder is already gone), with each kind distinguished in the selection list, and lets you pick by interactive multi-select, by name (`--skill <name>`, repeatable), or all at once (`--all`). Deletes the selected skill folders and drops their `.skills.toml` entries; `.skills.toml` is only rewritten when a tracked entry actually changes. The command is project-only — it never touches the library or git.
14+
15+
### Security
16+
17+
- The removal path refuses to follow a symlink: the destination's type is re-checked immediately before deletion, so a folder swapped for a symlink cannot redirect the recursive delete outside the project (closes a TOCTOU window surfaced by the pre-release audit). A symlinked destination is treated as "no folder on disk" — only its manifest entry can be dropped, never deleted through.
18+
- `--skill <name>` fails closed: an unknown name errors, and a name shared by two skills errors as ambiguous rather than silently deleting the wrong folder. The project root, `.git`, and `.skills.toml` can never be selected for deletion.
19+
20+
`skillctl remove` was reviewed before release with the project's standard multi-agent security audit pass (FS-safety / untrusted-input / logic dimensions). 5 new unit tests; `cargo test`: 158 pass; clippy clean; `cargo audit` clean.
21+
922
## [0.1.8] - 2026-05-25
1023

1124
### Security & robustness

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "skillctl"
3-
version = "0.1.8"
3+
version = "0.2.0"
44
edition = "2024"
55
rust-version = "1.85"
66
description = "CLI to manage your personal agent skills library across projects"

src/cli.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub enum Command {
3939
Pull(PullArgs),
4040
/// Find skills created locally and offer to add them to the library.
4141
Detect(DetectArgs),
42+
/// Remove skills from the current project (folder + any .skills.toml entry).
43+
Remove(RemoveArgs),
4244
}
4345

4446
#[derive(Args, Debug)]
@@ -199,6 +201,18 @@ pub struct DetectArgs {
199201
pub include_vendored: bool,
200202
}
201203

204+
#[derive(Args, Debug)]
205+
pub struct RemoveArgs {
206+
/// Skill to remove, by name. Repeatable. Mutually exclusive with --all.
207+
#[arg(long = "skill", value_name = "NAME", conflicts_with = "all")]
208+
pub skills: Vec<String>,
209+
210+
/// Remove every removable skill found in the project (installed via
211+
/// skillctl, created locally, or orphaned .skills.toml entries).
212+
#[arg(long, conflicts_with = "skills")]
213+
pub all: bool,
214+
}
215+
202216
#[derive(Clone, Copy, Debug, ValueEnum)]
203217
pub enum OnConflict {
204218
/// Replace the existing destination folder with the library version.

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ pub mod init;
55
pub mod list;
66
pub mod pull;
77
pub mod push;
8+
pub mod remove;
89
pub mod shared;

0 commit comments

Comments
 (0)