Skip to content

Commit 809214f

Browse files
os-zhuangclaude
andauthored
fix(create-objectstack): scope skill installs to the curated skills/ catalog (#3101)
* fix(create-objectstack): scope skill installs to the curated skills/ catalog The scaffolder (and docs) ran `npx skills add objectstack-ai/framework --all`, and the skills CLI's --all implies --skill '*', which includes metadata.internal skills — so the repo-internal dogfood-verification skill (.claude/skills/) landed in every scaffolded project's .agents/skills/ (15.1 third-party eval, P2). - scaffolder + docs now install from the /skills subpath, which scopes skills-CLI discovery to the published catalog - mark .claude/skills/dogfood-verification metadata.internal: true so interactive repo-root discovery hides it too - template-consistency ratchets: every SKILL.md outside skills/ must be metadata.internal, curated entries must not be, no customer-facing surface may advertise a repo-root install - scaffold-e2e: install from this checkout's skills/ subpath and assert set-equality with the curated catalog; assert repo-root listing surfaces exactly the curated set Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): strip ANSI codes before asserting the skills --list output The skills CLI colors the count even when piped (Found \e[32m9\e[39m skills), so the repo-root listing greps in the new skills-boundary step never matched in CI. The subpath-install set-equality assertion had already passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 021ba4c commit 809214f

11 files changed

Lines changed: 161 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-objectstack": patch
3+
---
4+
5+
Stop leaking repo-internal skills into scaffolded projects. The scaffolder (and the docs) advertised `npx skills add objectstack-ai/framework --all`, and the skills CLI's `--all` implies `--skill '*'` — which includes even `metadata.internal` skills — so repo-internal tooling like `.claude/skills/dogfood-verification` landed in every new project's `.agents/skills/`. All install commands are now scoped to the published catalog via the `/skills` subpath (`npx skills add objectstack-ai/framework/skills --all`), the internal skill is additionally marked `metadata.internal: true` to hide it from interactive discovery, and a template-consistency ratchet plus a scaffold-e2e assertion keep the boundary from regressing.

.claude/skills/dogfood-verification/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ description: >
88
"act as a real admin/user", "dogfood the Setup/Studio app", or browser-verify
99
a change in the running app. NOT a customer-published skill — this is internal
1010
agent tooling (lives in .claude/, never in the published `skills/` dir).
11+
metadata:
12+
# Hides this skill from interactive `npx skills add objectstack-ai/framework`
13+
# discovery. NOTE: the skills CLI's `--all` implies `--skill '*'`, which
14+
# includes internal skills — so the hard boundary against leaking into
15+
# customer projects is the `/skills` subpath in every advertised install
16+
# command (scaffolder, docs). Every SKILL.md outside `skills/` must carry
17+
# this marker — template-consistency.test.ts enforces both layers.
18+
internal: true
1119
---
1220

1321
# Dogfood verification

.github/workflows/scaffold-e2e.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,47 @@ jobs:
114114
curl -fsS http://localhost:8080/api/v1/ready
115115
kill "$SERVER_PID"
116116
117+
- name: 'Skills boundary: only the curated skills/ catalog installs'
118+
# 15.1 third-party eval: the repo-internal dogfood-verification skill
119+
# (.claude/skills/) leaked into scaffolded projects. Two leak paths,
120+
# two assertions against THIS checkout:
121+
# 1. The scaffolder/docs command is `skills add …/framework/skills
122+
# --all` — the /skills subpath is the hard boundary, because the
123+
# skills CLI's --all implies --skill '*' which INCLUDES
124+
# metadata.internal skills. Install from the subpath and assert
125+
# set-equality with the curated catalog.
126+
# 2. Interactive `skills add …/framework` (no --all) relies on
127+
# metadata.internal hiding — assert repo-root discovery surfaces
128+
# exactly the curated set and nothing internal.
129+
run: |
130+
cd "$RUNNER_TEMP"
131+
node "$GITHUB_WORKSPACE/packages/create-objectstack/bin/create-objectstack.js" skills-probe --skip-install --skip-skills
132+
cd skills-probe
133+
expected=$(cd "$GITHUB_WORKSPACE/skills" && ls -d objectstack-*/ | tr -d '/' | sort)
134+
expected_count=$(echo "$expected" | wc -l | tr -d ' ')
135+
136+
npx -y skills add "$GITHUB_WORKSPACE/skills" --all --copy
137+
installed=$(find . -name SKILL.md -not -path '*/node_modules/*' -exec dirname {} \; | xargs -rn1 basename | sort -u)
138+
echo "installed skills:"; echo "$installed"
139+
if [ "$installed" != "$expected" ]; then
140+
echo "::error::scaffolded skill set != curated skills/ catalog (internal-skill leak or missing skill)"
141+
printf 'expected:\n%s\n' "$expected"
142+
exit 1
143+
fi
144+
145+
# Strip ANSI color codes — the skills CLI colors the count even
146+
# when piped ("Found \e[32m9\e[39m skills"), which broke the greps.
147+
listing=$(npx -y skills add "$GITHUB_WORKSPACE" --list 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
148+
echo "$listing"
149+
if echo "$listing" | grep -q 'dogfood-verification'; then
150+
echo "::error::repo-root discovery surfaced an internal skill (metadata.internal marker missing?)"
151+
exit 1
152+
fi
153+
if ! echo "$listing" | grep -q "Found $expected_count skill"; then
154+
echo "::error::repo-root discovery does not match the curated catalog (expected $expected_count skills) — a SKILL.md outside skills/ is missing metadata.internal"
155+
exit 1
156+
fi
157+
117158
- name: Build official runtime image from this checkout
118159
# The scaffolded app's Dockerfile builds FROM
119160
# ghcr.io/objectstack-ai/objectstack. Build that base HERE from

content/docs/ai/skills-reference.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ npm create objectstack@latest my-app
2626
**Existing projects — install the whole bundle:**
2727

2828
```bash
29-
npx skills add objectstack-ai/framework --all
29+
npx skills add objectstack-ai/framework/skills --all
3030
```
3131

32-
Run the same command again at any time to pull the latest versions — `--all` is idempotent, so updating is one command regardless of how many skills the bundle contains. To choose which skills to add instead of the full set, run `npx skills add objectstack-ai/framework` without `--all`.
32+
Run the same command again at any time to pull the latest versions — `--all` is idempotent, so updating is one command regardless of how many skills the bundle contains. To choose which skills to add instead of the full set, run `npx skills add objectstack-ai/framework/skills` without `--all`. (The `/skills` subpath scopes discovery to the published catalog — always keep it in the command.)
3333

3434
<Callout type="info">
3535
Skills are versioned as **one bundle**. You do not track or update each skill individually — `--all` always installs the matching set for the `@objectstack/spec` version in your project.

content/docs/ai/skills.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Skills are distributed through the [skills.sh](https://skills.sh) registry from
3434
npm create objectstack@latest my-app
3535

3636
# Existing project — add the whole bundle (re-run to update)
37-
npx skills add objectstack-ai/framework --all
37+
npx skills add objectstack-ai/framework/skills --all
3838
```
3939

4040
The bundle is versioned and updated as **one unit**`--all` is idempotent, so you never track skills individually. For the full per-skill catalog and install options, see the [AI Skills Reference](/docs/ai/skills-reference#installing-skills).

content/docs/getting-started/build-with-claude-code.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ The scaffolder does more than copy files. It:
5656
- derives a **namespace** from the name (`support-desk``support_desk`), so
5757
every object you create is named `support_desk_*`;
5858
- installs dependencies;
59-
- runs `npx skills add objectstack-ai/framework --all` to install the **AI skills
59+
- runs `npx skills add objectstack-ai/framework/skills --all` to install the **AI skills
6060
bundle**;
6161
- writes an **`AGENTS.md`** (and `.github/copilot-instructions.md`) that teach your
6262
coding agent the project layout, the naming rules, and — critically — *to run

content/docs/getting-started/your-first-project.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ The scaffolder (`create-objectstack`) does four things:
4444
renamed to carry that prefix (`my_app_note`), which is what
4545
`os validate` later enforces.
4646
2. **Installs dependencies** (pnpm if available, otherwise npm).
47-
3. **Installs the AI skills bundle** (`npx skills add objectstack-ai/framework --all`)
47+
3. **Installs the AI skills bundle** (`npx skills add objectstack-ai/framework/skills --all`)
4848
so a coding agent is productive in the project from the first prompt.
4949
4. **Writes `AGENTS.md`** (and `.github/copilot-instructions.md`) with the
5050
project conventions — including the rule to run `npm run validate` after

packages/create-objectstack/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ offline.
5959
(`blank_note``my_app_note`).
6060
2. Installs dependencies (pnpm if available, otherwise npm).
6161
3. Installs the ObjectStack AI skills bundle for coding agents
62-
(`npx skills add objectstack-ai/framework --all`).
62+
(`npx skills add objectstack-ai/framework/skills --all` — scoped to the
63+
curated `skills/` catalog).
6364
4. Writes `AGENTS.md` and `.github/copilot-instructions.md` with the project
6465
conventions — unless the template ships its own.
6566

packages/create-objectstack/src/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
* - README.md first H1
2525
*
2626
* Finally we run `<pm> install` and (best-effort) install the ObjectStack
27-
* skills bundle via `npx skills add objectstack-ai/framework --all`.
27+
* skills bundle via `npx skills add objectstack-ai/framework/skills --all`.
28+
* The `/skills` subpath scopes discovery to the curated, customer-published
29+
* catalog — repo-internal skills (e.g. under `.claude/skills/`) must never
30+
* reach scaffolded projects.
2831
*/
2932

3033
import { Command } from 'commander';
@@ -477,15 +480,15 @@ const program = new Command()
477480
if (!options.skipInstall && !options.skipSkills) {
478481
printStep('Installing AI skills for your coding agent...');
479482
try {
480-
execSync('npx -y skills add objectstack-ai/framework --all', {
483+
execSync('npx -y skills add objectstack-ai/framework/skills --all', {
481484
stdio: 'inherit',
482485
cwd: targetDir,
483486
});
484487
console.log('');
485488
} catch {
486489
printWarning(
487490
'Skills installation skipped. Run manually:\n' +
488-
' npx skills add objectstack-ai/framework',
491+
' npx skills add objectstack-ai/framework/skills',
489492
);
490493
console.log('');
491494
}
@@ -507,7 +510,7 @@ const program = new Command()
507510
if (options.skipInstall || options.skipSkills) {
508511
console.log('');
509512
console.log(chalk.bold(' AI Skills (recommended):'));
510-
console.log(chalk.dim(' npx skills add objectstack-ai/framework'));
513+
console.log(chalk.dim(' npx skills add objectstack-ai/framework/skills'));
511514
}
512515
console.log('');
513516
} catch (error) {

packages/create-objectstack/src/template-consistency.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import { describe, it, expect } from 'vitest';
99
import fs from 'node:fs';
1010
import path from 'node:path';
11+
import { execFileSync } from 'node:child_process';
1112
import { fileURLToPath } from 'node:url';
1213
import { syncObjectStackDeps } from './pkg-utils.js';
1314

1415
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16+
const repoRoot = path.resolve(pkgRoot, '..', '..');
1517
const ownPkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
1618
const ownMajor = Number(ownPkg.version.split('.')[0]);
1719

@@ -82,6 +84,96 @@ describe('README template table', () => {
8284
});
8385
});
8486

87+
// Skills catalog boundary (15.1 third-party eval): scaffolded projects once
88+
// received the repo-internal `dogfood-verification` skill because the
89+
// scaffolder installed with a repo-wide `skills add … --all`, whose discovery
90+
// also walks `.claude/skills/`. The published catalog is exactly the root
91+
// `skills/` directory; everything else must stay repo-internal.
92+
describe('skills catalog boundary', () => {
93+
const frontmatterOf = (file: string): string =>
94+
/^---\n([\s\S]*?)\n---/.exec(fs.readFileSync(file, 'utf8'))?.[1] ?? '';
95+
const isMarkedInternal = (fm: string): boolean =>
96+
/^metadata:\s*$/m.test(fm) && /^ +internal:\s*true\s*(#.*)?$/m.test(fm);
97+
98+
const trackedSkillFiles = execFileSync('git', ['ls-files', '*SKILL.md'], {
99+
cwd: repoRoot,
100+
encoding: 'utf8',
101+
})
102+
.split('\n')
103+
.filter(Boolean);
104+
105+
it('finds the curated skills/ catalog (sanity)', () => {
106+
expect(
107+
trackedSkillFiles.filter((f) => f.startsWith('skills/')).length,
108+
).toBeGreaterThan(0);
109+
});
110+
111+
it('marks every SKILL.md outside skills/ as metadata.internal', () => {
112+
for (const rel of trackedSkillFiles) {
113+
if (rel.startsWith('skills/')) continue;
114+
expect(
115+
isMarkedInternal(frontmatterOf(path.join(repoRoot, rel))),
116+
`${rel} is outside the published skills/ catalog but is not hidden from ` +
117+
'the skills CLI. Add to its frontmatter:\n' +
118+
'metadata:\n internal: true\n' +
119+
'or move it into skills/ if it is meant for customers.',
120+
).toBe(true);
121+
}
122+
});
123+
124+
it('never marks a curated skills/ entry internal', () => {
125+
for (const rel of trackedSkillFiles) {
126+
if (!rel.startsWith('skills/')) continue;
127+
expect(
128+
isMarkedInternal(frontmatterOf(path.join(repoRoot, rel))),
129+
`${rel} is in the published catalog but marked metadata.internal — ` +
130+
'customers would silently stop receiving it.',
131+
).toBe(false);
132+
}
133+
});
134+
135+
it('scaffolder installs from the curated skills/ subpath, not the repo root', () => {
136+
expect(REGISTRY_SOURCE).toContain(
137+
'skills add objectstack-ai/framework/skills --all',
138+
);
139+
expect(REGISTRY_SOURCE).not.toMatch(
140+
/skills add objectstack-ai\/framework(?!\/skills)/,
141+
);
142+
});
143+
144+
// The /skills subpath is the hard boundary: the skills CLI's `--all`
145+
// implies `--skill '*'`, which INCLUDES metadata.internal skills — so any
146+
// customer-facing surface advertising a repo-root install would leak
147+
// internal skills again.
148+
it('no customer-facing surface advertises a repo-root skills install', () => {
149+
const surfaces = [
150+
'content/docs',
151+
'skills',
152+
'packages/create-objectstack',
153+
// this file mentions the bare form on purpose (needle + error message)
154+
':(exclude)packages/create-objectstack/src/template-consistency.test.ts',
155+
];
156+
let candidates = '';
157+
try {
158+
candidates = execFileSync(
159+
'git',
160+
['grep', '-nF', 'skills add objectstack-ai/framework', '--', ...surfaces],
161+
{ cwd: repoRoot, encoding: 'utf8' },
162+
);
163+
} catch {
164+
// git grep exits 1 on no matches — nothing to check then.
165+
}
166+
const rootInstalls = candidates
167+
.split('\n')
168+
.filter((line) => /skills add objectstack-ai\/framework(?!\/skills)/.test(line));
169+
expect(
170+
rootInstalls,
171+
'these lines advertise `skills add objectstack-ai/framework` without ' +
172+
'the /skills subpath — repo-root + --all installs internal skills',
173+
).toEqual([]);
174+
});
175+
});
176+
85177
describe('syncObjectStackDeps', () => {
86178
it('rewrites @objectstack/* ranges in deps and devDeps', () => {
87179
const pkg = {

0 commit comments

Comments
 (0)