`_has_skill()` in [`skill-reachability-reminder.py`](../../.agents/hooks/skill-reachability-reminder.py) returns true for **any** directory containing a `SKILL.md`. The contract asks a narrower question. The Proposed Surface row in [`cloud-executable.md`](../../docs/spec/cloud-executable.md) reads "at least one **kit skill** directory present at the repository's project-scope discovery directory, or at any user-scope discovery directory `install.py` targets", and `S-008`'s **Given** is "a clone
Scanned 9/3/2026
Install to Claude Code
npx -y skills add hams-ollo/zen-agent-skills --skill done --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Done?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/hams-ollo-done)More formats (shields.io, HTML) on the badges page.
---
id: bug-0021
title: The reachability bootstrap counts a foreign skill library as reachable, so it is silent in the cloud session it exists for
type: bug
status: done
priority: P1
parent: "ROADMAP Epic E #2: make this repository cloud-executable"
depends_on: []
spec: "docs/spec/cloud-executable.md"
scenarios: ["S-008", "S-010"]
touched_files:
- .agents/hooks/skill-reachability-reminder.py
- tests/test_hooks_reachability.py
- docs/spec/cloud-executable.md
created: 2026-08-08
---
## Problem
`_has_skill()` in [`skill-reachability-reminder.py`](../../.agents/hooks/skill-reachability-reminder.py)
returns true for **any** directory containing a `SKILL.md`. The contract asks a narrower question.
The Proposed Surface row in [`cloud-executable.md`](../../docs/spec/cloud-executable.md) reads "at least
one **kit skill** directory present at the repository's project-scope discovery directory, or at any
user-scope discovery directory `install.py` targets", and `S-008`'s **Given** is "a clone where no
**kit skill** is present at project scope or at any user-scope discovery directory".
So a machine carrying somebody else's skill library satisfies the implementation and not the
contract, and the hook stays silent in exactly the state `S-008` says it must speak.
Measured 2026-08-08 in a live cloud session on branch
`claude/developer-branch-review-j26u9s`, with none of this kit installed:
```text
$ echo '{"hook_event_name":"SessionStart","source":"startup","cwd":"/home/user/zen-agent-skills"}' \
| python3 .agents/hooks/skill-reachability-reminder.py
(no output)
$ ls ~/.claude/skills | head
algorithmic-art brain-dump brand-guidelines canvas-design
docx pdf pptx xlsx skill-creator theme-factory ... 24 total
$ reachable(repo_root, Path.home())
True
```
None of those 24 directories belongs to this kit. `doc-sync`, `fix-batch`, `house-review`,
`new-task`, `verifier-agent`, and `spec-author` are all absent, and the session proceeded with the
report suppressed.
**This falsifies a recorded prediction.**
[`cloud-executable.verification.md`](../../docs/spec/cloud-executable.verification.md) writes seven
falsifiable predictions before the Phase 4 proof run. Prediction 1 is *"The session reports `NO
SKILLS REACHABLE` at startup (`S-008`), falsified if nothing appears, or it appears when skills were
reachable."* Nothing appeared. The proof run is still owed, and running it against the current code
would produce a result nobody can read: "nothing appeared" would be indistinguishable between a
broken hook and a correct hook meeting a populated home.
**The failure shape is the one the hook was written to remove.** A cloud container that ships its own
`~/.claude/skills` is not an edge case, it is the default on the platform this epic targets, so the
committed `.claude/settings.json` exception buys nothing where it was granted. The module docstring
does say "it cannot tell whose skills it found", but that sentence only ships inside `REPORT`, the
string that never prints when the hook is silent.
## Scope
**In scope:** make `reachable()` answer the contract's question. Decide and implement how a kit skill
is recognised at a discovery directory, extend
[`test_hooks_reachability.py`](../../tests/test_hooks_reachability.py) with a case that fails against the
current hook, and reconcile `cloud-executable.md` with whatever is built.
**Out of scope:**
- Any environment detection. `S-016` forbids it, and a populated foreign home is not a cloud signal.
- Currency. `S-011` is unchanged: the hook answers reachability and never claims the skills are
current. Do not call `install.py --check` from the hook, for the reason its own docstring gives.
- Widening `.claude/settings.json` beyond the one reminder registered there. That exception is
written up in the conventions section of [`AGENTS.md`](../../AGENTS.md) and is not this task's to
bend.
- The Phase 4 proof run itself. This task makes its result readable; it does not run it.
## Implementation notes
Three candidate recognitions, and the choice is the task's real work. State which was taken and why
in the module docstring, since the next reader will ask.
1. **Name the kit's own skills.** A directory whose name matches one this repository ships. Exact and
self-contained, and it goes stale the moment the catalog changes unless it is derived at runtime,
which a hook cannot do without importing from this repository, forbidden by
[the hooks module contract](../../.agents/hooks/README.md).
2. **Read the install manifest.** Authoritative, and it makes a portable hook depend on one
repository's script layout, which the docstring already rejects for the currency question.
3. **Narrow the claim instead of the check.** Keep counting any skill, and report when none of *this
repository's* skills is present, using a marker the kit places at install time.
Whatever is chosen, the silence contract in `S-010` still holds: a session with the kit's skills
reachable receives nothing. Do not trade the noise property for the correctness one.
**The spec may need an amendment rather than the code moving to it.** If the honest answer is that a
hook cannot tell whose skills it found, then the contract is wrong and `S-008` needs rewriting, not
the hook. Per the convention recorded in [`docs/spec/README.md`](../../docs/spec/README.md), an
amendment to an approved spec keeps `status: approved`, carries a dated note, and is marked pending
the author's re-approval. Do not silently rewrite the scenario to match the code.
## Decisions
- **Candidate 1 was taken: a kit skill is recognised by directory name.** `KIT_SKILL_NAMES` in the
hook lists the twenty skills this kit ships, and one match at any discovery directory is
reachability.
- **Rejected, candidate 2, reading the install manifest.** It is authoritative and unusable here:
`scripts/.install-manifest.json` lives in the checkout that ran the installer, so a cloud clone has
none, which is the exact case being answered, and consulting it would tie a portable hook to one
repository's script layout.
- **Rejected, candidate 3, a marker placed at install time.** Retroactively wrong: every install that
already exists carries no marker, so the hook would cry wolf at all of them until re-run, which is
the failure the Risks section names. It also needs `install.py`, outside this task's
`touched_files`.
- **The staleness objection to candidate 1 is answered at test time, not runtime.** A hook may not
import from this repository, so it cannot derive the catalog; the repository can check the constant
against what it ships, and `test_the_recognised_names_are_exactly_the_skills_this_kit_ships`
asserts set equality in both directions. Adding, renaming, or removing a skill now means editing
that constant in the same commit.
- **Seam left open: the hook matches a name, not a provenance.** A foreign library shipping a
directory called `doc-sync` would silence it. Deliberate, and it is the cheap direction to be wrong
in for a reminder: the cost is one missing paragraph, against a false alarm that costs the adopter
their trust in the hook.
- **Rejected wording: a `NO KIT SKILLS REACHABLE` banner.** The report now reads `NO SKILLS REACHABLE
FROM THIS KIT`, keeping the original phrase at the head, because
[`cloud-executable.runbook.md`](../../docs/spec/cloud-executable.runbook.md) tells a person to look for
a message beginning with those exact words and the proof run it governs has not happened yet.
Retargeting that string silently would have broken the runbook from a file it does not name.
- **Premise qualified: `cloud-executable.md` needed no amendment, so it was left untouched.** The
task lists it in `touched_files` and offers an amendment branch, but the contract already asks the
narrower question in both places that matter (`S-008`'s Given and the Proposed Surface `Reachable`
row read "kit skill"). The code moved to the contract, so `status: approved` stands with no dated
note and the `docs/spec/README.md` re-approval queue is not extended.
## Risks and rollback
Touches the hooks module and an approved contract. The risk is a false positive in the other
direction: a recognition too narrow reports "no skills reachable" to an adopter who installed a
subset, and a reminder that cries wolf is uninstalled within a week, which costs more than the miss
it fixes. Cover the adopter-with-a-partial-install case in a test.
Reversible by reverting one commit; the hook writes nothing and holds no state.
## Acceptance criteria (mechanically verifiable)
python -m unittest discover -s tests -p "test_*.py" && python scripts/run-checks.py
- [ ] A test that places a foreign skill library at user scope, with no kit skill anywhere, and
asserts the hook reports. It must fail against the current hook.
- [ ] A test that places kit skills at user scope beside a foreign library and asserts silence
(`S-010`).
- [ ] A test that places kit skills at project scope only and asserts silence (`S-009`).
- [ ] The empty-discovery-directory case still counts as unreachable, so `--uninstall` leaving the
parent behind does not read as success.
- [ ] No environment detection is introduced; the existing `S-016` test still passes unchanged.
- [ ] The hook still writes nothing, exits 0 on malformed input, and imports nothing from this
repository.
- [ ] Either the code matches the contract's "kit skill" wording, or `cloud-executable.md` is amended
to match what a hook can honestly answer, dated and marked pending re-approval, with the
`docs/spec/README.md` re-approval queue extended.
- [ ] Existing tests still pass, unchanged in intent.
## Definition of done
- [ ] Acceptance command(s) pass locally.
- [ ] Conventions in AGENTS.md's conventions section followed.
- [ ] `doc-sync` run over the reader-facing documents and its findings applied or dismissed with a reason.
- [ ] File moved to `.tasks/done/`, `status: done`; one dated line added to `CHANGELOG.md` referencing this task id.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!