singular-skill 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +399 -2
- package/bin/on-expand.js +277 -0
- package/bin/on-prompt.js +672 -0
- package/bin/on-subagent.js +127 -0
- package/bin/singular-cli.js +52 -0
- package/bin/singular.js +114 -0
- package/config.example.json +23 -0
- package/docs/security.md +110 -0
- package/docs/spec.md +209 -0
- package/docs/testing.md +298 -0
- package/hooks/hooks.json +38 -0
- package/lib/cache.js +93 -0
- package/lib/cli-core.js +862 -0
- package/lib/commands/add.js +339 -0
- package/lib/commands/find.js +195 -0
- package/lib/commands/mode.js +93 -0
- package/lib/commands/status.js +128 -0
- package/lib/config.js +407 -0
- package/lib/context.js +184 -0
- package/lib/git.js +173 -0
- package/lib/inspect.js +183 -0
- package/lib/log.js +67 -0
- package/lib/refs.js +101 -0
- package/lib/registry.js +1152 -0
- package/lib/score.js +416 -0
- package/lib/skill-index.js +684 -0
- package/lib/stack.js +401 -0
- package/package.json +50 -4
- package/skills/singular/SKILL.md +24 -0
- package/skills/singular-find/SKILL.md +18 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "singular",
|
|
4
|
+
"description": "Marketplace for the Singular plugin (registered from this directory; plugin id singular@singular).",
|
|
5
|
+
"owner": { "name": "Nitya Sav" },
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "singular",
|
|
9
|
+
"source": "./",
|
|
10
|
+
"description": "Per-prompt skill routing, stack detection, and gated ephemeral discovery of third-party skills.",
|
|
11
|
+
"version": "0.2.0",
|
|
12
|
+
"category": "productivity"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "singular",
|
|
3
|
+
"displayName": "Singular",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Singular: per-prompt routing to installed skills, project stack detection, and allowlist-gated, commit-pinned, ephemeral loading of third-party skills from skills.sh.",
|
|
6
|
+
"author": { "name": "Nitya Sav" },
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": ["skills", "discovery", "hooks", "skills.sh", "routing"],
|
|
9
|
+
"hooks": "./hooks/hooks.json"
|
|
10
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nitya Sav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,400 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Singular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Singular is a Claude Code plugin with one command, `/singular`, that finds the right skill for the prompt you are typing. On every prompt it routes to the skills you already have installed and injects the project's stack (Next.js 16, Tailwind 4, TypeScript strict, pnpm, branch) as one factual line; when nothing local fits, it looks up third-party skills on [skills.sh](https://skills.sh), ranks them by relevance to your prompt (never sending the prompt itself), gates them by an owner allowlist, an install floor and the public audit, and offers them as a one-line suggestion. `/singular load owner/repo@skill` then loads that skill's text into the session, pinned to a commit and sanitized, without writing anything to `~/.claude/skills`; `/singular add` installs it persistently only after the folder has been inspected for anything that would turn a skill into a plugin. Zero npm dependencies, Node >= 20, every piece is a plain `node` script you can run by hand.
|
|
4
|
+
|
|
5
|
+
## Get started (30 seconds)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g singular-skill # install the CLI (or: npx singular-skill install)
|
|
9
|
+
singular install # register the skill with Claude Code
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open a new Claude Code session and type `/singular`.
|
|
13
|
+
|
|
14
|
+
`singular install` checks `node` (>= 20) and `claude` (>= 2.1.282), copies the plugin into `~/.claude/singular/plugin/` (so the path Claude Code holds stays valid across Node upgrades and `nvm` switches), registers that directory as the `singular` marketplace and installs `singular@singular` at user scope. It ends with `Installed. Open a new Claude Code session (or run /reload-plugins) and type /singular`. Nothing else on disk is touched: no `settings.json` edits, nothing under `~/.claude/skills`.
|
|
15
|
+
|
|
16
|
+
### No npm?
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/Nityasav/singular
|
|
20
|
+
cd singular
|
|
21
|
+
node bin/singular-cli.js install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Same result; `node bin/singular-cli.js` is the `singular` binary. To try the plugin for one session without installing anything: `claude --plugin-dir /path/to/singular` (it shows up as `singular@inline`).
|
|
25
|
+
|
|
26
|
+
Requirements for the remote features: `git` on `PATH` (used for commit pinning and for `/singular add`), network access to `skills.sh` and `github.com`. `npx` runs only when a skill's *text* is fetched (`/singular load`, `mode: "auto"`, `pinnedSkills`, the background prefetch), always as the pinned `skills@1.5.26`; `/singular add` uses `git` only.
|
|
27
|
+
|
|
28
|
+
## Use it
|
|
29
|
+
|
|
30
|
+
`/singular` is the only user-facing command. Everything after it is a subcommand (`/singular help` lists them). Prompts that are not slash commands need nothing from you: the hook runs on its own (see "How it works").
|
|
31
|
+
|
|
32
|
+
### `/singular` or `/singular status`
|
|
33
|
+
|
|
34
|
+
Shows the configuration, effective mode, how many installed skills were indexed, the stack of the current directory, cache statistics, ephemeral pins, persistent installs and the last log lines, followed by the one-screen help. Claude runs `node "${CLAUDE_PLUGIN_ROOT}/bin/singular.js" status` through the Bash tool and relays the output verbatim (observed 2026-10-13 with a fresh state dir, from the Next.js fixture project; paths shortened):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
[singular] singular 0.2.0 plugin root: /Users/you/.claude/singular/plugin
|
|
38
|
+
state dir: /Users/you/.claude/singular
|
|
39
|
+
config: /Users/you/.claude/singular/config.json
|
|
40
|
+
mode: "suggest"
|
|
41
|
+
allowlist: ["anthropics","vercel-labs","microsoft","multica-ai"]
|
|
42
|
+
installFloor: 1000
|
|
43
|
+
maxLocalSkills: 3
|
|
44
|
+
maxCandidates: 2
|
|
45
|
+
minPromptChars: 40
|
|
46
|
+
remoteTimeoutMs: 2500
|
|
47
|
+
totalBudgetMs: 6000
|
|
48
|
+
contextBudgetChars: 2000
|
|
49
|
+
cacheTtlHours: 24
|
|
50
|
+
skillsCliVersion: "1.5.26"
|
|
51
|
+
requireAuditPass: true
|
|
52
|
+
stackDetection: true
|
|
53
|
+
blockedRefs: []
|
|
54
|
+
pinnedSkills: []
|
|
55
|
+
prefetch: true
|
|
56
|
+
installed skills indexed: 169 (synced 13, user 2, plugin 154)
|
|
57
|
+
stack (/Users/you/some-project): Next.js 16, React 19, Tailwind 4, Supabase, Stripe, Vitest, Playwright, TypeScript 5 strict, pnpm; branch main
|
|
58
|
+
cache: 0 entries, 0.0 KB, newest -, oldest - (/Users/you/.claude/singular/cache)
|
|
59
|
+
loaded pins (/singular load, ephemeral): 0
|
|
60
|
+
persistent installs (/singular add): 0
|
|
61
|
+
log: /Users/you/.claude/singular/singular.log
|
|
62
|
+
|
|
63
|
+
Singular — find, load, add and manage third-party skills for Claude Code
|
|
64
|
+
/singular status + this help
|
|
65
|
+
... (the help shown under /singular help)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
After a few prompts the cache line fills up (`28 entries, 414.9 KB, newest 0 min ago`), the log line is followed by the last 5 JSON lines of `singular.log`, and each ephemeral pin is listed as ` <ref> -> <40-hex sha> loads N last <ISO time>`. Problems appear as ` ! ...` lines after the config keys, for example `! config.json is not valid JSON (...); using defaults`, `! mode: must be one of off|local|suggest|auto; using default "suggest"`, `! SINGULAR_MODE=auto ignored (the environment can only lower the mode below "suggest")`, or, when the environment lowers the mode, `effective mode "off" (file says "suggest"; ...)`. With a skills.sh cooldown: `skills.sh: rate-limited (HTTP 429) — no requests until HH:MM; cached catalogs still rank`. `/singular status --json` prints the same facts as one JSON object (no help trailer).
|
|
69
|
+
|
|
70
|
+
### `/singular help`
|
|
71
|
+
|
|
72
|
+
Prints the one-screen help (`bin/singular.js help`; no network):
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Singular — find, load, add and manage third-party skills for Claude Code
|
|
76
|
+
/singular status + this help
|
|
77
|
+
/singular status [--json] config, mode, indexed skills, cache, pins, installs, log tail
|
|
78
|
+
/singular find <query> search skills.sh (gated: owner allowlist, install floor, audit); never installs
|
|
79
|
+
/singular load owner/repo@skill [--update]
|
|
80
|
+
load a skill's text into this session only (commit-pinned, sanitized; nothing
|
|
81
|
+
is written to ~/.claude/skills); owner/repo#<40-hex-sha>@skill pins a commit
|
|
82
|
+
/singular add owner/repo@skill persistent install into ~/.claude/skills (pinned, inspected before and after the copy)
|
|
83
|
+
/singular mode [off|local|suggest|auto]
|
|
84
|
+
show or switch the mode (rewrites only the "mode" key of config.json)
|
|
85
|
+
/singular help this help
|
|
86
|
+
Config: ~/.claude/singular/config.json (allowlist, blockedRefs, installFloor, pinnedSkills, ...).
|
|
87
|
+
From a terminal: `singular <subcommand> ...` (npm install -g singular-skill) or `node bin/singular.js <subcommand> ...`.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
An unknown subcommand (`/singular turbo`) prints `[singular] unknown subcommand "turbo"; expected one of status | help | find | load | add | mode` followed by the same help, and exits 1.
|
|
91
|
+
|
|
92
|
+
### `/singular find <query>`
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
/singular find playwright flaky tests
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Searches installed skills first, then skills.sh through the same gates the hook uses. This is the one path that sends your query words to skills.sh (you typed them on purpose). Claude runs `node "${CLAUDE_PLUGIN_ROOT}/bin/singular.js" find "playwright flaky tests"` and summarises the report in three parts: installed matches, the gated candidate table, and the rejected count with reasons. The report itself (observed 2026-10-08 for `playwright` with the catalogs already cached: 5 requests served from cache, 12 ms; a cold `supabase auth` query made 7 live requests in 1.5 s):
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
[singular] query: "playwright" keywords: playwright
|
|
102
|
+
|
|
103
|
+
Installed locally (already available, use the Skill tool / slash command):
|
|
104
|
+
vercel:access-protected-vercel-deployment plugin score 4.369 Access and test Vercel deployments protected by ...
|
|
105
|
+
|
|
106
|
+
Third-party candidates (gated: allowlist, install floor, audit; NOT installed):
|
|
107
|
+
ref installs audit allowlisted?
|
|
108
|
+
anthropics/skills@webapp-testing 172K warn yes
|
|
109
|
+
microsoft/playwright-cli@playwright-cli 177K warn yes
|
|
110
|
+
|
|
111
|
+
Load ephemerally (text only): /singular load <ref>
|
|
112
|
+
Install persistently (pinned): /singular add <ref>
|
|
113
|
+
|
|
114
|
+
Rejected: 527
|
|
115
|
+
305 x installs N < floor N
|
|
116
|
+
170 x owner not allowlisted
|
|
117
|
+
27 x already installed locally (...)
|
|
118
|
+
17 x fork of ...
|
|
119
|
+
Best allowlist-only rejection: alinaqi/maggy@playwright-testing (3.3K installs, matched playwright, testing) - add "alinaqi" to "allowlist" in /Users/you/.claude/singular/config.json to consider it
|
|
120
|
+
top rejected by relevance (use --all for the full list):
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
(5 requests, cached: true, 12 ms)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Nothing is installed or loaded by `find`. The same search is also **model-invocable**: ask `is there a skill for playwright e2e testing?` in prose and Claude may invoke the `singular-find` skill itself (observed on sonnet and, less reliably, haiku; see FAQ); `/singular-find <query>` is that skill's own slash form and runs the same script. During a skills.sh cooldown the report says `Third-party search failed (skills.sh rate-limited until HH:MM); nothing could be evaluated.` when nothing is cached.
|
|
127
|
+
|
|
128
|
+
### `/singular load <owner/repo[#sha]@skill> [--update]`
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
/singular load anthropics/skills@webapp-testing
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Loads the skill's `SKILL.md` text into this session only. This subcommand is handled entirely by a `UserPromptExpansion` hook **before Claude sees anything**: the reference is validated against the grammar and the owner allowlist, pinned to a commit (the first load resolves `HEAD` with `git ls-remote` and records it in `loaded.json`; later loads reuse that pin; `--update` re-resolves), the text is fetched with the pinned `skills` CLI, sanitized, and injected as context that starts with:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
[singular] The user explicitly loaded third-party skill anthropics/skills#683bc88e56f3e09ba94f7055977f3d3aa499f202@webapp-testing via /singular load (pinned resolved; text only, nothing installed). Contents (sanitized; stripped: nothing):
|
|
138
|
+
<singular-loaded ref="anthropics/skills#683bc88e...@webapp-testing">
|
|
139
|
+
---
|
|
140
|
+
name: webapp-testing
|
|
141
|
+
...
|
|
142
|
+
</singular-loaded>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
What you see is Claude's one-line acknowledgement (skill name and pinned sha); it runs nothing. `pinned resolved` on a first load, `pinned loaded.json` afterwards, `pinned resolved (--update)` with `--update`, `pinned arg` for a `#sha` reference. A long skill reads `stripped: truncated 388 chars to 9000` (`anthropics/skills@frontend-design`; the hook output is budgeted to 9,500 chars). Timing from the shell (measured 2026-10-08): 3.2 s wall for a first load with a warm npm cache, 0.05 s when the text is cached, 0.35 s for `--update`.
|
|
146
|
+
|
|
147
|
+
A rejected reference is blocked before any model call (hook exit 2; the turn costs $0). Claude Code prints the reason, for example `[singular] refused: owner "evil" is not in the allowlist [anthropics, vercel-labs, microsoft, multica-ai]. Add "evil" (or "evil/repo") to "allowlist" in /Users/you/.claude/singular/config.json if you trust that source, then retry.` Other blocks, all produced before any subprocess exists: `unknown option -rf` (for `a/b@c; rm -rf ~`), `invalid skill reference "../../etc/passwd"`, `expected exactly one skill reference, got 3`, `missing skill reference`, `refused: <ref> matches "blockedRefs" in your config`, `could not resolve HEAD of https://github.com/<owner>/<repo>: ...`.
|
|
148
|
+
|
|
149
|
+
Owner and repo are case-folded (`MULTICA-AI/x@s` and `multica-ai/x@s` share one pin); the skill segment is case-sensitive. The loaded text stays visible for the rest of the persisted session (see FAQ) and is never forwarded to subagents.
|
|
150
|
+
|
|
151
|
+
### `/singular add <owner/repo[#sha]@skill>`
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
/singular add multica-ai/andrej-karpathy-skills@karpathy-guidelines
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Persistent install into `~/.claude/skills/<skill>`, pinned to a commit. Claude runs `node "${CLAUDE_PLUGIN_ROOT}/bin/singular.js" add <ref> --yes` (you typed the command, so `--yes` is the confirmation) and shows the output verbatim. The script refuses an existing target before any network call, fetches the pinned commit with `git` into a temp dir, checks the tree with `git ls-tree` (symlinks, exec bits, submodules), inspects the folder **before** anything is copied, copies with a before/after snapshot of `~/.claude/skills`, and inspects the copy again:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
INSTALLED multica-ai/andrej-karpathy-skills#2c606141936f1eeef17fa3043a72095b4765b9c2@karpathy-guidelines sha 2c606141936f1eeef17fa3043a72095b4765b9c2
|
|
161
|
+
path: /Users/you/.claude/skills/karpathy-guidelines (1 files, inspected before and after the copy: clean)
|
|
162
|
+
source: multica-ai/andrej-karpathy-skills skills/karpathy-guidelines
|
|
163
|
+
audit: { "status": "pass", "providers": [ ... ] }
|
|
164
|
+
Run /reload-skills or start a new session to use it.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A folder that would become a plugin (hooks, `bin/`, MCP, agents, executable files, symlinks, capability-granting frontmatter in any spelling, shell forms) prints `REJECTED: <pinned ref>` with one problem per line and `Nothing was written to ~/.claude/skills.` (verified against a local bare repository built from `test/fixtures/skill-folders/malicious`). Other outcomes, all exit 1 with nothing written: `REFUSED: ~/.claude/skills/<skill> already exists (directory). Remove it yourself first; singular never overwrites.`, `REJECTED: owner "evil" is not allowlisted [...]`, `no skill named "DOCX" in anthropics/skills at 683bc88 (exact, case-sensitive). Found: ...`, `git fetch failed (exit 128): ...` for a bogus sha, `could not resolve HEAD of ...: repository not found or private (GitHub asked for credentials)`.
|
|
168
|
+
|
|
169
|
+
### `/singular mode [off|local|suggest|auto]`
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
/singular mode local
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Shows the effective mode and where it comes from, or rewrites **only** the `mode` key of `config.json` (every other key, including unknown ones, is preserved). Takes effect on the next prompt, no restart. Without an argument:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
[singular] mode: suggest (config file: /Users/you/.claude/singular/config.json)
|
|
179
|
+
usage: /singular mode <off|local|suggest|auto>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With one: `[singular] mode: suggest -> local (/Users/you/.claude/singular/config.json). Takes effect on the next prompt; no restart needed.` Errors (exit 1): `unknown mode "turbo"; expected one of off | local | suggest | auto`; `refusing to write mode "auto" into <redirected>/config.json: auto is honoured only in the default ~/.claude/singular/config.json (unset SINGULAR_HOME first)`; `<path>/config.json is not valid JSON (...); fix it by hand first`. `/singular mode auto` also prints the warning quoted under "Modes". Live through `claude -p` (haiku, measured 2026-10-07) each call cost $0.0014-0.0018; on `turbo` the model reported the failure and did not pick another mode.
|
|
183
|
+
|
|
184
|
+
### From a terminal
|
|
185
|
+
|
|
186
|
+
Every subcommand also works without Claude: `singular status`, `singular find <query>`, `singular load <ref>`, `singular add <ref>`, `singular mode <m>` proxy to the same scripts (see "CLI reference"). The per-prompt hook can be dry-run too:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
node ~/.claude/singular/plugin/bin/on-prompt.js --dry-run "<your prompt>" --cwd ~/some-project --verbose
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
prints exactly what would be injected, the timings, which words (if any) were sent to skills.sh, and the skip reason when nothing is injected.
|
|
193
|
+
|
|
194
|
+
## How it works
|
|
195
|
+
|
|
196
|
+
1. **Every prompt of 40+ characters** fires the `UserPromptSubmit` hook (`bin/on-prompt.js`). It indexes your installed skills (user, project, plugin and claude.ai-synced; 18-52 ms in-process for 168 skills, measured 2026-10-08), scores them against the prompt (BM25-style, name tokens weighted), reads the project's manifests and `.git/HEAD` for the stack (no subprocesses), and injects one short factual note, for example:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
[singular] Stack: Next.js 16, React 19, Tailwind 4, Supabase, Stripe, Vitest, Playwright, TypeScript 5 strict, pnpm; branch main
|
|
200
|
+
Installed skills likely relevant: vercel:access-protected-vercel-deployment (matches playwright, tests), vercel:deployments-cicd (matches ci)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Slash commands, prompts under 40 characters, prompts starting with `[singular]` and the synthetic `<task-notification>` / `<system-reminder>` prompts are skipped. One run per prompt, never per tool call. Subagents get only the `Stack:` line through a `SubagentStart` hook (they do not inherit the parent's context).
|
|
204
|
+
|
|
205
|
+
2. **On a local miss** in `suggest` (the default) or `auto` mode, the hook fetches each allowlisted owner's skills.sh catalog (cached 24 h) and runs at most two searches for single words from your prompt that are in a built-in tech lexicon (`q=playwright`, `q=e2e`); free-form prompt text never leaves the machine. Candidates are ranked by relevance plus `log10(installs)`, then gated: owner allowlist, `blockedRefs`, install floor (1000), fork dedupe, already-installed names, audit status. The result is two lines of context and a one-line notice in your transcript:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Third-party candidates (not loaded): anthropics/skills@webapp-testing (172K installs, audit warn) — load with /singular load anthropics/skills@webapp-testing; ...
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
[singular] 2 third-party skills may fit this prompt (not loaded): /singular load anthropics/skills@webapp-testing, /singular load microsoft/playwright-cli@playwright-cli
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
A detached background process prefetches the top candidate's text so the next step is instant. Cold discovery measured 1.2-2.4 s (catalogs plus two searches), warm 5-32 ms (2026-10-08).
|
|
216
|
+
|
|
217
|
+
3. **You decide.** `/singular load <ref>` loads the text (pinned, sanitized, session-only); `/singular add <ref>` installs it persistently after inspection; `/singular find <query>` shows the full gated and rejected lists when a pick looks off. Nothing third-party reaches Claude without one of these, unless you turn on `auto`.
|
|
218
|
+
|
|
219
|
+
4. **State lives in `~/.claude/singular/`**: `config.json`, `cache/`, `loaded.json` (ephemeral pins), `installed.json` (persistent installs), `singular.log`, and the installed plugin copy under `plugin/`. The hooks never write anywhere else; `/singular add` is the only path that writes under `~/.claude/skills`, and only after inspection.
|
|
220
|
+
|
|
221
|
+
## Modes
|
|
222
|
+
|
|
223
|
+
Switch with `/singular mode <off|local|suggest|auto>` or edit `~/.claude/singular/config.json`. `SINGULAR_MODE=local claude` lowers the mode for one shell; the environment can **only lower** it (a repository's committed `.claude/settings.json` `env` reaches the hook too, verified on 2.1.293; `docs/security.md` section 4), and `auto` counts only when it is written in the default config file.
|
|
224
|
+
|
|
225
|
+
| mode | per-prompt hook | network | third-party text reaches Claude |
|
|
226
|
+
| :-- | :-- | :-- | :-- |
|
|
227
|
+
| `off` | no-op (`--dry-run` prints `[singular] (no context: mode off)`) | no | never (only `/singular load` still works) |
|
|
228
|
+
| `local` | stack + installed-skill routing | no | only via `/singular load` |
|
|
229
|
+
| `suggest` **(default)** | stack + routing + on a local miss: allowlisted-owner catalogs (cached 24 h) + up to 2 single tech-word searches, ranking, gating, audit, a **text suggestion**, then a detached background prefetch of the top candidate's text | yes | only after you type `/singular load` |
|
|
230
|
+
| `auto` | everything above, then the single best gated candidate's SKILL.md is pinned, fetched, sanitized and **injected without confirmation** | yes | **yes, automatically** |
|
|
231
|
+
|
|
232
|
+
> **Warning about `auto`.** Anthropic's guidance is to use skills only from sources you trust; measured base rates for public registries include ~13% critical findings and live trojanized packages, install counts are not a safety signal, and scanners are advisory and bypassable (`docs/security.md`). In `auto`, text written by a third party is placed in Claude's context every time your prompt has no strong local match, with only the allowlist, install floor, audit status, commit pin and sanitizer between you and it. The residual risk is prompt injection in prose you did not read. Singular shows a one-time warning when `auto` first injects something, never writes the text to disk, and strips the capability-granting parts of SKILL.md, but it cannot make untrusted prose trustworthy. Keep `suggest` unless you have read the skills your allowlist can reach.
|
|
233
|
+
|
|
234
|
+
`/singular mode auto` prints: `[singular] mode "auto": third-party skill text from allowlisted skills.sh sources will be injected into your prompts WITHOUT confirmation (text only, pinned, sanitized, still untrusted prose). Cold prompts add up to ~5 s and up to 9,000 chars of third-party text per prompt; see README "What auto actually costs". Run /singular mode suggest to go back.`
|
|
235
|
+
|
|
236
|
+
### What `auto` actually costs
|
|
237
|
+
|
|
238
|
+
| situation | what happens | added time | added context |
|
|
239
|
+
| :-- | :-- | :-- | :-- |
|
|
240
|
+
| cold prompt, candidate never seen on this machine | HEAD is resolved (`git ls-remote`, ~0.5-1.8 s); if >= 4.5 s of the 6 s budget remain, `npx skills use` fetches the text inline (~4.2 s measured with a warm npm cache), otherwise only the **suggestion** is emitted and the text is prefetched in the background. In practice the first auto prompt on a machine yields only the suggestion (measured 2026-10-07: cold `discoverMs` 3,368 then `autoSkipped=only 1924ms left`); the next one is served from the cache | ~1-6 s | 0 or up to 9,000 chars |
|
|
241
|
+
| the same candidate again (or after the prefetch finished) | text served from the immutable per-sha cache; no git, no npx | ~0 ms | up to 9,000 chars of third-party text **on every such prompt** |
|
|
242
|
+
| a strong local match exists | nothing remote, same as `suggest` | 0 | 0 |
|
|
243
|
+
|
|
244
|
+
### Always-on guidelines (`pinnedSkills`)
|
|
245
|
+
|
|
246
|
+
Behavioural skills (Karpathy-style coding guidelines) have no task nouns, so keyword routing only finds them when the prompt happens to say "refactor" or "simplify". For "apply these guidelines to every coding prompt", list them in `config.json`:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
"pinnedSkills": ["andrej-karpathy-skills:karpathy-guidelines", "multica-ai/andrej-karpathy-skills@karpathy-guidelines"]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
An **installed** skill name (plain or `plugin:name`) becomes a one-line hint; an allowlisted **ref** is pinned like `/singular load`, fetched once, cached forever by sha, and its sanitized text injected. Either is emitted **once per session**, not on every prompt. Non-allowlisted refs and unknown names are ignored with a warning in `/singular status`.
|
|
253
|
+
|
|
254
|
+
### What `suggest` decides
|
|
255
|
+
|
|
256
|
+
The remote step runs only if `mode` is `suggest`/`auto` **and** no installed skill is a *strong* match: score >= 10 and at least two meaningful matched tokens, or a single *name* token that is a tech noun (`nextjs`, `stripe`, `pdf`, `auth`), or a rare word with score >= 20. A single hit on a hyper-common word (`file`, `function`, `build`, `page`, `test`, `claude`, `code`, `plugin`, `skill`, ...) is never listed and never suppresses discovery; a single common English name word (`design`, `route`, `system`, `review`, `plan`, `setup`, `status`) is listed but never strong. `--dry-run --verbose` prints `local=name:score*` (`*` = strong) and `weak(dropped)=…`; a golden routing table over a 168-skill snapshot lives in `test/bin.test.js`. Examples measured 2026-10-08 from the Next.js fixture project (hook time is in-process; add ~100 ms of `node` startup):
|
|
257
|
+
|
|
258
|
+
| prompt | result | hook time |
|
|
259
|
+
| :-- | :-- | :-- |
|
|
260
|
+
| `what are the tradeoffs between server and client components in next.js 16? cite the docs` | stack + `vercel:nextjs` (strong), `vercel:next-cache-components` (strong); remote skipped | 25 ms |
|
|
261
|
+
| `build a landing page with tailwind css!!` (exactly 40 chars) | stack + `vercel:shadcn` (strong), `figma:figma-generate-design`; remote skipped | 19 ms |
|
|
262
|
+
| the same prompt minus one `!` (39 chars) | skipped: `prompt shorter than minPromptChars (39 < 40)` | < 1 ms |
|
|
263
|
+
| `add a /api/health route that returns uptime json` | stack + `vercel:routing-middleware` (route; not strong) → discovery sent `q=api`, `q=json`: `anthropics/skills@claude-api (69.6K, warn)`, a loose fit | 2,424 ms |
|
|
264
|
+
| `our playwright end-to-end tests are flaky on CI, help me stabilise them with retries and proper waits` | stack + three weak locals, then `anthropics/skills@webapp-testing (172K, warn)`, `microsoft/playwright-cli@playwright-cli (177K, warn)`; sent `q=playwright`, `q=e2e` | 2,090 ms |
|
|
265
|
+
| `write a terraform module for an s3 bucket with versioning and lifecycle rules` | stack only; discovery sent `q=terraform`, `q=s3`, 0 candidates (no allowlisted owner has one) | 1,240 ms |
|
|
266
|
+
| a 12,000-char prompt | stack + three strong design-system matches; 289-320 chars of context | 20 ms |
|
|
267
|
+
|
|
268
|
+
A catalog request that times out is remembered for 5 minutes (`fail:catalog:<owner>` marker), a stale copy keeps ranking, and the next prompt measured 6 ms (2026-10-07).
|
|
269
|
+
|
|
270
|
+
## Configuration reference
|
|
271
|
+
|
|
272
|
+
`~/.claude/singular/config.json` (or `$SINGULAR_HOME/config.json`). Created from the defaults on first run; unknown keys are kept; a wrong type falls back to the default and is reported by `/singular status`. `config.example.json` in this repository is identical to the defaults.
|
|
273
|
+
|
|
274
|
+
| key | default | meaning |
|
|
275
|
+
| :-- | :-- | :-- |
|
|
276
|
+
| `mode` | `"suggest"` | `off` / `local` / `suggest` / `auto` (see "Modes") |
|
|
277
|
+
| `allowlist` | `["anthropics","vercel-labs","microsoft","multica-ai"]` | GitHub owners (`owner`) or repositories (`owner/repo`) whose skills may be suggested, loaded or installed. Case-insensitive, exact. Everything else is rejected as `owner X not allowlisted` |
|
|
278
|
+
| `installFloor` | `1000` | minimum skills.sh install count for a candidate |
|
|
279
|
+
| `maxLocalSkills` | `3` | installed skills named per prompt |
|
|
280
|
+
| `maxCandidates` | `2` | third-party candidates named per prompt (suggest) / considered (auto: only the first is loaded) |
|
|
281
|
+
| `minPromptChars` | `40` | shorter prompts get no context at all (inclusive: 40 chars route, 39 do not; measured after stripping Claude Code's `<pasted_content>` wrapper lines) |
|
|
282
|
+
| `remoteTimeoutMs` | `2500` | per-request timeout for skills.sh |
|
|
283
|
+
| `totalBudgetMs` | `6000` | hard deadline for the whole per-prompt hook; the remote step is skipped when < 1500 ms remain, an inline `auto` fetch when < 4500 ms remain; a watchdog flushes the local-only context if anything overruns (hook timeout is 8 s) |
|
|
284
|
+
| `contextBudgetChars` | `2000` | cap for the suggestion text (candidates are dropped first, then installed matches). Loaded skill text (auto / `pinnedSkills` / `/singular load`) is capped separately at 9,000 chars and fitted so the whole hook output stays under 9,500 chars, because Claude Code only previews hook output above 10,000 chars |
|
|
285
|
+
| `cacheTtlHours` | `24` | TTL for owner catalogs, tech-word searches, audit verdicts and resolved HEAD SHAs (`$SINGULAR_HOME/cache/`). Skill text is cached **forever** by pinned ref: a commit is immutable, so any ref is fetched at most once per machine |
|
|
286
|
+
| `skillsCliVersion` | `"1.5.26"` | the `skills` npm CLI version used for `skills use` (text fetch; pinned; never `latest`). `/singular add` does not use the CLI |
|
|
287
|
+
| `requireAuditPass` | `true` | drop candidates whose skills.sh audit status is `fail` (or any provider `CRITICAL`). `warn` and `unknown` still pass with the status shown, because a strict all-pass rule would reject `anthropics/skills@webapp-testing` and several vercel-labs and microsoft skills that carry a `warn` today |
|
|
288
|
+
| `stackDetection` | `true` | read `package.json`/lockfiles/`tsconfig.json`/`pyproject.toml`/`requirements.txt`/`go.mod`/`Cargo.toml`/`Gemfile`/`pom.xml`/`.git/HEAD` in the session's cwd |
|
|
289
|
+
| `blockedRefs` | `[]` | never suggest/load/install these: `owner`, `owner/repo` or `owner/repo@skill`; wins over the allowlist |
|
|
290
|
+
| `pinnedSkills` | `[]` | always-on skills, injected once per session (see "Always-on guidelines") |
|
|
291
|
+
| `prefetch` | `true` | in `suggest`, spawn a detached `on-expand.js <ref> --prefetch` for the top candidate so its HEAD and text are cached before you type `/singular load`. `false` disables the background process |
|
|
292
|
+
|
|
293
|
+
Environment (untrusted: a repository's committed `.claude/settings.json` `env` block reaches the hook process, so the environment can only **lower** privilege): `SINGULAR_HOME` (state directory, default `~/.claude/singular`; honoured only as an absolute path outside the project directory, and `mode: "auto"` is never honoured in a redirected home), `SINGULAR_MODE` (per-process mode override; can only lower the mode below the config file's value), `CLAUDE_CONFIG_DIR` (honoured when indexing installed skills and by the `singular` CLI), `SINGULAR_DEBUG=1` (log the extracted keywords). `/singular status` lists every override and whether it was ignored, and names any active **test seam** (`SINGULAR_REGISTRY_BASE`, `SINGULAR_GIT_BASE`, `SINGULAR_SKILLS_DIR`, `SINGULAR_TEST_ALLOW_AUTO`): those exist only for the test suite and are documented in `docs/testing.md`.
|
|
294
|
+
|
|
295
|
+
State files under the state directory: `config.json`, `cache/`, `loaded.json` (ephemeral pins: `{ref, sha, ts, lastTs, loads, chars, stripped}` per unpinned ref), `installed.json` (persistent installs made by `/singular add`), `singular.log` (JSON lines, rotated at 2 MB), `.auto-mode-warned`, `spill-*.md` (last-resort overflow of oversized routing text), and `plugin/` (the installed plugin copy, managed by the `singular` CLI; `plugin.prev/` is the previous copy kept for rollback).
|
|
296
|
+
|
|
297
|
+
### Adding an allowlist owner
|
|
298
|
+
|
|
299
|
+
1. Decide whether you trust the **owner** (a GitHub user or org) or only one **repository**. `"expo"` allows every repo under `github.com/expo`; `"expo/skills"` allows only that one.
|
|
300
|
+
2. Edit `~/.claude/singular/config.json`:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
"allowlist": ["anthropics", "vercel-labs", "microsoft", "multica-ai", "expo/skills"]
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
3. No restart needed: every hook run re-reads the config. `/singular status` shows the effective list and any parse warning. `/singular find <query>` names the most relevant entry that was refused only for its owner (`Best allowlist-only rejection: ... - add "<owner>" to "allowlist" in ~/.claude/singular/config.json to consider it`). Read the repository before adding an owner.
|
|
307
|
+
4. `blockedRefs` wins over the allowlist, so you can allow an owner and still block one of its skills.
|
|
308
|
+
|
|
309
|
+
## CLI reference
|
|
310
|
+
|
|
311
|
+
`singular` (from `npm install -g singular-skill`, or `node bin/singular-cli.js` in a checkout) manages the installation and proxies the subcommands to a terminal. Zero dependencies; `--help` prints this list. Every subprocess is `execFile` with an argument array, never a shell string; `CLAUDE_CONFIG_DIR` is passed through to `claude`, so a different Claude Code config directory is honoured.
|
|
312
|
+
|
|
313
|
+
| command | what it does |
|
|
314
|
+
| :-- | :-- |
|
|
315
|
+
| `singular install [--dir <path>] [--dry-run] [--json]` | (1) checks `node >= 20` and `claude` on `PATH` with version `>= 2.1.282` (`claude --version`); (2) copies the plugin payload (`.claude-plugin`, `hooks`, `skills`, `bin`, `lib`, `package.json`, `LICENSE`, `README.md`, `config.example.json`) from the installed package to `~/.claude/singular/plugin/` (written to `plugin.tmp` then renamed; the previous copy is kept as `plugin.prev` for rollback); (3) `claude plugin marketplace add ~/.claude/singular/plugin`, or `claude plugin marketplace update singular` when that marketplace already exists; (4) `claude plugin install singular@singular`, or `claude plugin update singular@singular` when already installed; (5) prints `Installed. Open a new Claude Code session (or run /reload-plugins) and type /singular`. `--dir` changes the payload destination; `--dry-run` prints the plan without running anything; `--json` prints the steps as JSON. On any failure it exits 1, names the exact `claude plugin ...` command that failed and how to retry it, and never leaves a half-registered state |
|
|
316
|
+
| `singular update` | same as `install`: re-copies the payload from the currently installed npm package, then `marketplace update` + `plugin update`. Run it after `npm update -g singular-skill` |
|
|
317
|
+
| `singular uninstall [--purge]` | `claude plugin uninstall singular@singular`, `claude plugin marketplace remove singular`, removes `~/.claude/singular/plugin`. `--purge` also removes the config, cache, log and pins (the whole `~/.claude/singular`) |
|
|
318
|
+
| `singular status` | node and claude versions, payload path and version, whether the marketplace is registered and the plugin installed/enabled (from `claude plugin list --json`), config path and mode, the last 5 `singular.log` lines |
|
|
319
|
+
| `singular doctor` | `status` plus an actionable hint for each problem it finds (claude missing or too old, marketplace not registered, plugin not installed or disabled, mode `auto`, config warnings) |
|
|
320
|
+
| `singular find <query>` · `singular add <ref>` · `singular mode [m]` · `singular load <ref>` | proxies to `bin/singular.js` (`load` to the direct CLI mode of `bin/on-expand.js`, which prints the sanitized text to stdout), so the same commands work from a terminal with the same output and exit codes |
|
|
321
|
+
|
|
322
|
+
The underlying scripts remain callable directly from the installed copy (`P=~/.claude/singular/plugin`):
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
node $P/bin/singular.js status [--json] [--cwd DIR]
|
|
326
|
+
node $P/bin/singular.js help
|
|
327
|
+
node $P/bin/singular.js find "playwright flaky tests" [--limit N] [--all] [--json] [--cwd DIR]
|
|
328
|
+
node $P/bin/singular.js add anthropics/skills@frontend-design [--yes] [--json]
|
|
329
|
+
node $P/bin/singular.js mode [off|local|suggest|auto] [--json]
|
|
330
|
+
node $P/bin/singular.js load multica-ai/andrej-karpathy-skills@karpathy-guidelines [--update] [--json] # terminal form of /singular load: prints the sanitized text
|
|
331
|
+
node $P/bin/on-expand.js multica-ai/andrej-karpathy-skills@karpathy-guidelines [--update] [--json] # what /singular load injects
|
|
332
|
+
node $P/bin/on-prompt.js --dry-run "build a landing page in nextjs with tailwind" [--cwd DIR] [--verbose] [--json]
|
|
333
|
+
node $P/bin/on-subagent.js --dry-run [--cwd DIR]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## FAQ
|
|
337
|
+
|
|
338
|
+
**Why are there no third-party candidates?** In order of likelihood: (1) an installed skill was a strong match, so the remote step was skipped (`--dry-run --verbose` shows `local=name:score*`, `*` = strong, and no `discover=` line); (2) every ranked entry was rejected: `/singular find <query>` shows the reasons: `owner not allowlisted` (by far the largest group), `installs N < floor 1000`, `already installed locally (...)` (a candidate whose name matches an installed skill is dropped in favour of the local one), `fork of ...`, `duplicate skill name`, audit `fail`; (3) no allowlisted owner publishes anything whose *name* contains a word from your prompt (terraform, s3), and names only match on exact stemmed tokens; (4) the mode is `local`/`off`, the prompt is under 40 characters or starts with `/`, or skills.sh is in a 5-minute 429 cooldown (`/singular status`); (5) a request timed out (`errors` in `--dry-run --json`; that owner is then skipped for 5 minutes and a stale catalog keeps ranking).
|
|
339
|
+
|
|
340
|
+
**Why did it not suggest X?** Run `/singular find <the words you expected to match>`. If X appears under `Best allowlist-only rejection`, its owner is not in your allowlist. If X is listed under `fork of ...`, an allowlisted source publishes the same skill name and won. If X is not in the list at all, skills.sh did not return it for the words searched: the hook sends only single tech-lexicon words (`shared=[...]` in `--dry-run --verbose`), and the API is lexical and capped at 200 results per query, so a repository whose skill names contain neither `skill` nor one of your tech words is unreachable from the hook (use `/singular find`, which also sends the joined query). Install counts come last in the ranking: a 315-install entry that matches two words outranks a 314K-install entry that matches one.
|
|
341
|
+
|
|
342
|
+
**How do I add an owner?** Edit `~/.claude/singular/config.json` and append `"owner"` (or `"owner/repo"` for one repository) to `allowlist`; no restart. `/singular find` prints the exact owner to add. Adding an owner means every skill under that GitHub account can be suggested, loaded and installed through the gates; read what you add. Never let Claude edit the allowlist for you (the skill bodies tell it not to).
|
|
343
|
+
|
|
344
|
+
**Is auto mode safe?** No, and it is not meant to be: it places third-party prose in Claude's context without you reading it. The gates (allowlist, floor, audit, pin, sanitizer) remove executable and capability-granting parts, not persuasion. `auto` is only honoured in the default config file, can never be enabled by a repository or the environment, prints a one-time warning, and `/singular mode suggest` reverts it. Use it only with an allowlist whose skills you have read.
|
|
345
|
+
|
|
346
|
+
**Which models relay suggestions?** Candidates are placed in Claude's context together with an instruction to end the reply with one line `Load with /singular load <ref>` when one fits. That is advice to the model, not a guarantee: measured 2026-10-07 across three live explorations, the model ended its reply with a load line in 2 of 6 runs that had candidates (sonnet and haiku, a Playwright prompt whose candidates fit well; none of the four runs for a pandas prompt), and in 1 of 2 haiku runs after the instruction was moved before the candidate list. The hook's own one-line `[singular] N third-party skill(s) may fit this prompt (not loaded): /singular load ...` notice in your transcript is the reliable channel; a relay by the model is a bonus. Likewise the model-invocable `singular-find` skill: for `is there a skill for playwright e2e testing?` sonnet invoked it; haiku reached for `WebSearch`/`WebFetch` instead in one measured run (`docs/testing.md`, case 13). Typing `/singular find ...` yourself always works.
|
|
347
|
+
|
|
348
|
+
**Does `/singular load` persist across turns?** Yes, for the whole persisted session (`--resume`/`--continue` included): Claude Code stores hook `additionalContext` in the transcript as an attachment of type `hook_additional_context` (verified: section titles and a verbatim bullet of `karpathy-guidelines` were recalled two turns later; a second load coexisted with the first). It is not re-sent each turn, so it does not cost context twice. It does not survive `/compact` or `--no-session-persistence` (re-run `/singular load` after `/compact`), and it is never forwarded to subagents. Long skills are truncated at 9,000 chars; `/singular add` keeps the full text on disk.
|
|
349
|
+
|
|
350
|
+
**Does the hook see every skill I have?** CLI-bundled skills (`dataviz`, `update-config`, `code-review`, `simplify`, …) have no `SKILL.md` on disk, so they are never indexed or listed; Claude still picks them on its own. Suggested installed skills are not invoked automatically either: in measured sonnet runs the model never called the Skill tool for a listed skill, but it did reflect the injected stack (Tailwind 4 classes, a Next 16 doc citation).
|
|
351
|
+
|
|
352
|
+
**What leaves the machine?** In `suggest`/`auto`: each allowlisted owner's catalog (`GET skills.sh/api/search?q=skill&owner=<owner>&limit=200`, no prompt content, cached 24 h), at most two single tech-lexicon words from your prompt (`q=stripe`), and audit lookups by candidate id. `/singular find` additionally sends your query's keywords as one joined search. Loading a skill runs `git ls-remote` and a pinned `npx skills use` against github.com. `mode: "local"` makes the hook network-free. Full table in `docs/security.md` section 5; `--dry-run --verbose` prints `shared=[...]`, the exact words sent.
|
|
353
|
+
|
|
354
|
+
## Troubleshooting
|
|
355
|
+
|
|
356
|
+
| Symptom | Check |
|
|
357
|
+
| :-- | :-- |
|
|
358
|
+
| `singular install` says `claude: command not found` (or `claude is not on PATH`) | Install Claude Code first (`npm install -g @anthropic-ai/claude-code` or the native installer) and make sure `claude --version` prints in the same shell. The CLI calls `claude` from `PATH`; nothing is registered until it is found |
|
|
359
|
+
| `singular install` says the Claude Code version is too old | The hook contracts need Claude Code >= 2.1.282 (`UserPromptExpansion` with `command_args`, `SubagentStart`). Update Claude Code (`claude update`) and re-run `singular install` |
|
|
360
|
+
| `claude plugin list` does not show `singular@singular` | Run `singular doctor`: it reports whether the `singular` marketplace is registered and whether the plugin is installed and enabled, with the command to fix each. `singular install` is idempotent: run it again. A session loaded with `--plugin-dir` shows as `singular@inline` only in `claude --plugin-dir <dir> plugin list` |
|
|
361
|
+
| `/singular` says `Unknown command` or does nothing | The session predates the install: run `/reload-plugins` or start a new session. `/singular:singular ...` (the namespaced form) is equivalent |
|
|
362
|
+
| `/singular load` expands but no `[singular] The user explicitly loaded ...` block appears | The expansion hook did not run. `disableAllHooks` in settings disables it; otherwise look at `~/.claude/singular/singular.log` for `on-expand`. Run `singular load <ref>` from a terminal to see the same sanitized text |
|
|
363
|
+
| `UserPromptExpansion operation blocked by hook: ... not in the allowlist` | Intended. Add the owner to `allowlist` if you trust it (see "Adding an allowlist owner") |
|
|
364
|
+
| Hook timeout (`singular.log` shows the run was cut, or nothing was injected on a slow network) | The per-prompt hook has 8 s (6 s internal budget) and the load hook 25 s. A cold `npx` download of the `skills` CLI can take most of the 25 s the first time, so retry `/singular load` once; a slow skills.sh owner costs `remoteTimeoutMs` once per 5 minutes and is then skipped. Lower `totalBudgetMs`, set `mode: "local"`, or disable `stackDetection` if prompts feel slow (`--dry-run --verbose` prints `indexMs`, `localMs`, `discoverMs`, `autoFetchMs`) |
|
|
365
|
+
| `could not resolve HEAD` / `could not fetch` | `git ls-remote https://github.com/<owner>/<repo> HEAD` and `npx -y skills@1.5.26 use <ref>` from your shell; check network/proxy. `repository not found or private (GitHub asked for credentials)` means the repository does not exist under that owner or is private |
|
|
366
|
+
| `/singular find` says `Third-party search failed (skills.sh rate-limited until HH:MM)` | skills.sh answered HTTP 429. Nothing is wrong locally; cached catalogs still rank. Wait for the time shown (`/singular status` shows it too) |
|
|
367
|
+
| Nothing appears in context | `/singular status`: is `mode` `off`? Is the prompt >= 40 chars and not a slash command? `node ~/.claude/singular/plugin/bin/on-prompt.js --dry-run "<prompt>" --cwd <project> --verbose` prints the skip reason. In an interactive session hook context is not printed as a message; use `claude --verbose` or `-p --output-format stream-json --verbose --include-hook-events` |
|
|
368
|
+
| `mode` is `auto` in the file but `/singular status` says `suggest` | `auto` counts only in the default `~/.claude/singular/config.json`; a redirected `SINGULAR_HOME` or `SINGULAR_MODE` can only lower the mode |
|
|
369
|
+
| `/singular add` printed `REJECTED` | The downloaded folder contained plugin components, non-plain files in the git tree or capability frontmatter; nothing was written. Use `/singular load` for the text only, or vet the repository by hand |
|
|
370
|
+
| Config warnings in `/singular status` | A key has the wrong type or the JSON is invalid; the default is used for that key. Fix the file; no restart needed |
|
|
371
|
+
| I want to reset | `singular uninstall --purge` (or `rm -rf ~/.claude/singular` for the state only) |
|
|
372
|
+
|
|
373
|
+
Logs: `~/.claude/singular/singular.log` (one JSON line per hook run with timings, matches, candidates, errors). Hook stdout/stderr are also in Claude Code's debug output (`claude --debug`).
|
|
374
|
+
|
|
375
|
+
## Uninstall
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
singular uninstall # claude plugin uninstall singular@singular, marketplace remove singular, delete ~/.claude/singular/plugin
|
|
379
|
+
singular uninstall --purge # also delete the config, cache, log and pins (~/.claude/singular)
|
|
380
|
+
npm uninstall -g singular-skill
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Skills installed with `/singular add` live under `~/.claude/skills/<skill>` as ordinary skills and are **not** removed by either command; delete them yourself.
|
|
384
|
+
|
|
385
|
+
## Testing
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
npm test # unit suite: node --test 'test/*.test.js'; no network, no real npx; ~15 s
|
|
389
|
+
npm run e2e # live: runs claude -p with the plugin loaded via --plugin-dir (needs a login; costs money)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`docs/testing.md` is the testing guide: the file list and test count, what each file covers, the coverage table, the e2e isolation recipe (`test/e2e/README.md`), the test seams (loopback registry, local git base, redirected skills dir, auto in a redirected home) and the cost of live runs. The e2e runner never touches `~/.claude/settings*.json`, `~/.claude/skills`, `~/.claude/plugins` or the real `~/.claude/singular`, and verifies that with before/after fingerprints.
|
|
393
|
+
|
|
394
|
+
## Security
|
|
395
|
+
|
|
396
|
+
`docs/security.md` has the threat model, the eleven gates in the order they apply, why ephemeral loading is the default, how the environment and a repository's settings are treated (they can only lower privilege), the exact outbound requests, what the hooks never do, and the residual risks you accept. Short version: the hooks never write under `~/.claude/skills`, `~/.claude/plugins` or any settings file; never run a shell string; validate every reference with a strict grammar before any subprocess exists; strip capability-granting frontmatter and shell forms from loaded text; and never block a prompt (only a rejected `/singular load` is blocked, before any model call). `docs/spec.md` is the module contract.
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT.
|