feat(skill): default generated skills to ~/.agents/skills with Claude Code symlink (#125)

* feat(skill): default generated skills to ~/.agents/skills with Claude Code symlink

Personal (user-level) installs of generated book skills now land in the
cross-agent root ~/.agents/skills/<slug>/ for every host, so one copy
serves Copilot CLI, Amp, and Claude Code. Claude Code does not scan that
root, so Step 10 creates a symlink at ~/.claude/skills/<slug> pointing
to the generated skill. Host-private and project-local destinations stay
available on explicit request, and a pre-existing real directory at
~/.claude/skills/<slug> triggers a migration offer instead of a silent
replace.

* fix(skill): guard the Claude Code symlink and scope discovery claims

Skip symlink creation when a real directory remains at
~/.claude/skills/<skill_name> (ln -sfn would nest the link inside it and
Claude Code would keep loading the stale copy), report discovery only
for the destination actually used, use <skill_name> consistently for
the skill folder placeholder, and clarify in README and how-it-works
that the symlink is created when running under Claude Code.

* docs: state the Claude Code symlink as attempted-and-verified, not a fact

Align README and the how-it-works pipeline diagram with Step 10's
read-back rule: the symlink is attempted and verified, and the run
report states the actual result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skill): keep Hermes Agent on its own personal root under the new default

Hermes Agent landed after this branch was opened. Its personal root is
partitioned by category ($HERMES_HOME/skills/<category>) and it does not
scan the cross-agent root, so it is the one host the collapse cannot serve
from ~/.agents/skills.

Carry it forward rather than regress it: the Step 5 table keeps the Hermes
row, a personal Hermes install uses that root and takes no symlink, and the
"Discoverable by" line names Hermes alone for it - no cross-agent claim,
since no other host scans the Hermes root. The intro sentence no longer
says one copy serves every host, because with Hermes carved out that is
not true. README and the pipeline diagram say the same.

* fix(skill): name the relocation target as the final skill directory

`mv <src> <dest>` was grammatical under both readings and only one worked.
With <dest> read as a root, the printed line breaks the symlink the run had
just created: the skill lands at <root>/<skill_name> while `ln -sfn <root>`
points one level up, so Claude Code resolves to a directory with no
SKILL.md.

The placeholder is now <dest_root>/<skill_name>, so the move target names
the final skill directory and there is one reading left. The fill rule says
so explicitly, shows the broken form, and requires the resolved paths be
substituted rather than the placeholders printed.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Anton Novoselov
2026-09-12 02:40:03 +01:00
committed by GitHub
parent ecf1860605
commit 349dc4365b
3 changed files with 78 additions and 18 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ Works with any host that supports the open [Agent Skills](https://github.com/age
## 📦 What it generates
Running `/book-to-skill your-book.pdf` (or a folder, glob, or list of files) creates a full skill in your agent's skills directory (`~/.copilot/skills/<slug>/` for Copilot CLI, `~/.agents/skills/<slug>/` for Amp or cross-agent, `~/.claude/skills/<slug>/` for Claude Code, or `$HERMES_HOME/skills/<category>/<slug>/` for Hermes Agent):
Running `/book-to-skill your-book.pdf` (or a folder, glob, or list of files) creates a full skill in the user-level cross-agent skills directory `~/.agents/skills/<slug>/` — one copy that Copilot CLI, Amp, and Codex discover natively; when run under Claude Code, the converter also attempts a verified symlink at `~/.claude/skills/<slug>/` Claude Code sees the skill when the link read-back confirms it; otherwise the run report says so. Hermes Agent partitions its personal skills by category and does not scan the cross-agent root, so a Hermes install lands in `$HERMES_HOME/skills/<category>/<slug>/` instead (host-private and project-local destinations remain available on request):
| File | Purpose | Size |
|------|---------|------|
+69 -13
View File
@@ -78,7 +78,7 @@ This converter can run from multiple skill systems. When looking for this conver
9. Hermes Agent personal skills: `$HERMES_HOME/skills/` (defaults to `~/.hermes/skills/`)
10. Hermes Agent project skills: `.hermes/skills/` or `.agents/skills/`
For **generated** book skills, pick a destination that the user's host agent can actually discover (see Step 5). When more than one valid root exists, ask the user once and remember the answer for the session — do not silently default.
For **generated** book skills, prefer the user-level cross-agent root `~/.agents/skills/` — one physical copy serves every supported host. Copilot CLI and Amp discover it natively; Claude Code needs a symlink from `~/.claude/skills/<skill_name>` (created in Step 10, see Step 5 for the rules). Pick a host-private or project-local root only when the user asks for one.
---
@@ -338,26 +338,28 @@ Otherwise, propose two options and let the user choose:
Default to author-concept format if the book has a strong methodological identity.
Choose the destination skill root (`SKILLS_HOME`). Probe the user's filesystem for existing skill homes and pick by **the host the user is running in**:
Choose the destination skill root (`SKILLS_HOME`). For **personal** (user-level) installs, default to the cross-agent root `~/.agents/skills` — one physical copy that every host except Hermes Agent reaches, natively or through a symlink:
| Host agent | Personal skill root (probe in order) | Project-local root |
| Host agent | Personal skill root | Project-local root |
|---|---|---|
| **GitHub Copilot CLI** | `~/.copilot/skills` `~/.agents/skills` | `.github/skills``.claude/skills``.agents/skills` |
| **Amp** | `~/.agents/skills` `~/.config/agents/skills``~/.config/amp/skills` | `.agents/skills` |
| **Claude Code** | `~/.claude/skills` | `.claude/skills` |
| **GitHub Copilot CLI** | `~/.agents/skills` (discovered natively) | `.github/skills``.claude/skills``.agents/skills` |
| **Amp** | `~/.agents/skills` (discovered natively) | `.agents/skills` |
| **OpenAI Codex** | `~/.agents/skills` (discovered natively; follows symlinks) | `.agents/skills` |
| **Hermes Agent** | `$HERMES_HOME/skills/<category>` (defaults to `~/.hermes/skills/<category>`) | `.hermes/skills/<category>``.agents/skills` |
| **Claude Code** | `~/.agents/skills` + symlink from `~/.claude/skills/<skill_name>` | `.claude/skills` |
For Hermes Agent, use the active profile's `HERMES_HOME` and choose a category that matches the generated skill's subject. Do not construct profile paths manually. If the user selects a project-local Hermes root, run `hermes skills trust <project-root>` after generation and verify discovery with `hermes skills list`; project skills remain unavailable until the project is trusted.
Hermes Agent is the one host that keeps its own personal root: it partitions personal skills by category and does not scan the cross-agent root. Use the active profile's `HERMES_HOME` and choose a category that matches the generated skill's subject. Do not construct profile paths manually. If the user selects a project-local Hermes root, run `hermes skills trust <project-root>` after generation and verify discovery with `hermes skills list`; project skills remain unavailable until the project is trusted.
Selection rules:
1. If **exactly one** of the host's candidate roots exists on disk, use it without asking.
2. If **none** exist (fresh machine), ask the user which root to create — present the host-appropriate options and remember the choice for the session. Do not silently pick.
3. If the user explicitly asked for project-local output, prefer the project-local row.
4. If you cannot identify the host, ask: "Which agent are you running this in — Hermes Agent, GitHub Copilot CLI, Amp, Codex, or Claude Code?"
1. Personal install: set `SKILLS_HOME` to `~/.agents/skills` (create the directory if missing). One exception, so the default does not invent a convention in someone else's house: if `~/.agents/skills` does not exist **and** the host's private root already contains skills, use the private root instead and say why in the report.
2. **Claude Code does not scan `~/.agents/skills`** — after generation completes, Step 10 links the skill in with `ln -sfn "$HOME/.agents/skills/<skill_name>" "$HOME/.claude/skills/<skill_name>"`.
3. **Hermes Agent personal installs use the Hermes row above**, not the cross-agent root, and take no symlink.
4. If the user explicitly asks for a host-private root (`~/.copilot/skills`, `~/.claude/skills`, `~/.config/agents/skills`, `~/.config/amp/skills`), honor it and skip the symlink.
5. If the user explicitly asked for project-local output, use the project-local row for their host.
6. If the choice requires knowing the host (project-local output, the Hermes personal root, or the Claude Code symlink) and you cannot identify it, ask: "Which agent are you running this in — Hermes Agent, GitHub Copilot CLI, Amp, Codex, or Claude Code?"
Set `SKILLS_HOME` to the selected root and check if `$SKILLS_HOME/<skill_name>/` already exists.
If it does, prompt the user to choose:
Set `SKILLS_HOME` to the selected root and check if `$SKILLS_HOME/<skill_name>/` already exists. On Claude Code, also check whether `~/.claude/skills/<skill_name>` exists as a **real directory** (not a symlink) — a previous install may live there; if so, offer to migrate it (move the directory into `~/.agents/skills/` and replace the original path with the symlink) before continuing.
If the skill already exists, prompt the user to choose:
1. **Update / Fold-in** (Mode 4) — integrate new files/content into the existing skill components.
2. **Overwrite** — delete and regenerate the skill from scratch.
3. **Rename** — append `-2` or use a different custom slug.
@@ -576,6 +578,38 @@ If the scanner exits non-zero, stop and ask a human to review its file/line find
## Step 10 — Cleanup and report
If the host is Claude Code and `SKILLS_HOME` is `~/.agents/skills` (the default personal install), expose the skill to Claude Code with a symlink — Claude Code only scans `~/.claude/skills`:
```bash
mkdir -p "$HOME/.claude/skills"
LINK="$HOME/.claude/skills/<skill_name>"
TARGET="$HOME/.agents/skills/<skill_name>"
if [ -d "$LINK" ] && [ ! -L "$LINK" ]; then
CLAUDE_STATUS="skipped-realdir" # Step 5 migration declined; leave the old dir
else
ln -sfn "$TARGET" "$LINK" 2>/dev/null || true
# Read the link back — do NOT trust that `ln` did what was asked. On Windows/MSYS
# `ln -s` may COPY instead of link (or need Developer Mode / an elevated shell), and
# PowerShell/cmd have no `ln` at all. The report must reflect what is on disk, not the
# fact that the command ran.
if [ -L "$LINK" ] && [ "$(readlink "$LINK")" = "$TARGET" ]; then
CLAUDE_STATUS="linked"
elif [ -e "$LINK" ]; then
CLAUDE_STATUS="copy" # a real file/dir landed instead of a link
else
CLAUDE_STATUS="absent" # ln unavailable or refused
fi
fi
```
The real-directory guard is required: `ln -sfn` into an existing real directory would nest the link *inside* it (`~/.claude/skills/<skill_name>/<skill_name>`), leaving Claude Code loading the stale copy. If the user declined the Step 5 migration, skip the symlink and say so in the report — Claude Code keeps using the old directory until it is migrated.
**Read the link back before you report anything about it.** The symlink is a claim, not a fact: fill the "Discoverable by" line from `CLAUDE_STATUS` (what is actually on disk), never from "the command was issued". **Do not hard-fail when the link is missing or is a copy** — the skill exists at the hub and every other host still finds it; the honest report is "written to `~/.agents/skills/<skill_name>`; Claude Code will not see it until the link is created", not an abort. (Windows lead, unverified: a directory junction — `mklink /J` in an elevated `cmd`, or `New-Item -ItemType Junction` in PowerShell — needs neither Developer Mode nor a symlink privilege; if you attempt it, it does not change the read-back-then-report rule.)
Skip this when the user chose a host-private or project-local root (Step 5, rules 3-4).
Then clean up the extraction workdir:
```bash
PYTHON_BIN="${PYTHON_BIN:-python3}"
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
@@ -635,6 +669,14 @@ Usage:
Ask <skill_name> about <topic> → find and explain a topic
Ask <skill_name> for ch<N> → dive into a specific chapter
Discoverable by: <only what is true for the chosen destination — see below>
Somewhere else? mv ~/.agents/skills/<skill_name> <dest_root>/<skill_name> \
&& ln -sfn <dest_root>/<skill_name> ~/.claude/skills/<skill_name>
Prompted for permission on every file? That is your host gating writes outside the
working directory. Say "save it in this project" and re-run to write inside it.
Reload (if your agent doesn't auto-detect new skills):
GitHub Copilot CLI: /skills reload
Claude Code: restart the session
@@ -646,6 +688,20 @@ Share this skill (optional):
Copilot ecosystem: gh skill publish $SKILLS_HOME/<skill_name>
```
Fill the "Discoverable by" line from `CLAUDE_STATUS` (the read-back result), never from the fact that `ln` ran — for `~/.agents/skills` installs:
- `linked` → "Copilot CLI, Amp, Codex (natively); Claude Code via symlink ~/.claude/skills/<skill_name>"
- `skipped-realdir` → "Copilot CLI, Amp, Codex (natively); **NOT** Claude Code — migrate the real directory at ~/.claude/skills/<skill_name> first"
- `copy` or `absent` → "Copilot CLI, Amp, Codex (natively); **NOT** Claude Code — the host could not create the symlink (a plain copy drifts on the next Update/Fold-in). Enable Developer Mode / create the link manually, or run the skill from ~/.agents/skills"
- Hermes Agent personal root → "Hermes Agent (from `$HERMES_HOME/skills/<category>`)"; no symlink claim, and no cross-agent claim, because the other hosts do not scan the Hermes root
- other host-private or project-local root → name only the host(s) that scan that root; no symlink claim
The "Somewhere else?" relocation line must be correct for the path actually taken, so it never breaks the symlink the run just created. **`mv` always targets the final skill directory, `<dest_root>/<skill_name>`, never `<dest_root>` itself.** `mv ~/.agents/skills/mybook ~/.copilot/skills && ln -sfn ~/.copilot/skills ~/.claude/skills/mybook` reads as valid and is not: the skill lands at `~/.copilot/skills/mybook` while the link points one level up at the root, so Claude Code resolves to a directory with no `SKILL.md`, which is the exact breakage this line exists to avoid. Substitute the destination the user actually named, so the printed command carries real paths and there is nothing left to interpret:
- `~/.agents/skills` + symlink → `mv ~/.agents/skills/<skill_name> <dest_root>/<skill_name> && ln -sfn <dest_root>/<skill_name> ~/.claude/skills/<skill_name>`
- host-private root, Hermes Agent included → `mv <src_root>/<skill_name> <dest_root>/<skill_name>`
- project-local root → `mv <project_root>/<skill_name> <dest_root>/<skill_name>`
The "Prompted for permission on every file?" line is the answer to a host that gates writes outside the working directory (any personal-scope root is out-of-cwd): the destination was announced above, and the one-line fix — re-run asking for the project-local root — sits next to it. Keep it only for personal-scope installs; drop it when the user already chose project-local.
---
## Step 11 — Publish the generated skill to GitHub (optional)
+8 -4
View File
@@ -36,10 +36,14 @@ scripts/extract.py <paths…> --mode <technical|text>
Generates master SKILL.md with core mental models
Skill written to one of:
~/.copilot/skills/<slug>/ (GitHub Copilot CLI)
~/.agents/skills/<slug>/ (Copilot CLI or Amp, cross-agent)
~/.claude/skills/<slug>/ (Claude Code)
Skill written to:
~/.agents/skills/<slug>/ (cross-agent: Copilot CLI, Amp, Codex)
~/.claude/skills/<slug>/ (symlink → above, attempted under
Claude Code and verified by
read-back; report states the result)
$HERMES_HOME/skills/<category>/<slug>/
(Hermes Agent: its own personal root,
not the cross-agent one, no symlink)
/tmp/book_skill_work/ 🗑️ cleaned up
```