onboard-project / references/what-gets-written.md

What gets written, and how to take it back

A reference shipped with the skill · 1,144 words

What gets written, and how to take it back

Everything this skill can write, where it lands, and the undo. Nothing here happens without one explicit yes, and every line of it is meant to be removable by somebody who has forgotten this skill exists.

What Where Written by Undo
The project's skill list skills-profile.json at the repository root (committed placement) apply --yes Delete it, or git revert
The same list, kept out of the repository <agents dir>/project-profiles/<encoded repo path>-<hash>.json (local placement) apply --yes Delete that file
The routing every session loads .claude/rules/skill-routing.md apply --yes Delete it, or git revert
A git exclude line for that file .git/info/exclude, or the shared one in a worktree (local placement only) — a row of its own in the change list apply --yes Remove the line
The user's placement defaults <agents dir>/onboard-project.json You, by hand Delete or edit it
The session-start check A SessionStart hook in the user's settings.json install-check-hook.mjs, run by the user install-check-hook.mjs --remove
A one-time "onboarding exists" marker <agents dir>/project-profiles/<encoded repo path>-<hash>.json check, once per repository Delete that file

<agents dir> is the Skills CLI's own directory under the user's home, where its global lock file already lives.

The local file name is Claude Code's own project-directory encoding followed by a short hash of the full path. The encoding alone collides — work/foo-bar and work/foo/bar are one name — and in 1.0.0 it was the whole name, so two repositories could share one profile. A local profile also records the repository it belongs to, and one naming a different repository is never read.

A 1.0.0 local profile at the old name is not read: it carries no repository, and its name is the collision itself, so nothing can say which repository wrote it. A repository onboarded locally with 1.0.0 is onboarded again, and the old file can be deleted.

The marker and a profile can live at the same name, and are told apart by content: a profile has a skill list, and the marker never does. Only the check reads the marker.

The profile

{
  "version": 1,
  "generatedBy": "onboard-project 1.0.0",
  "scannedAt": "2026-09-14T23:50:00Z",
  "placement": "committed",
  "fingerprint": "sha256 over the repository signals that evaluated true",
  "evidence": {
    "release-notes": {
      "signals": ["exists:.changeset/", "exists:CHANGELOG.md", "json:package.json#version"],
      "true": ["json:package.json#version"]
    }
  },
  "skills": {
    "release-notes": {
      "match": "strong",
      "evidence": ["package.json has a version"],
      "useWhen": "about to cut a release, bump a version, or write a CHANGELOG entry",
      "required": true,
      "scope": "project"
    }
  },
  "declined": {
    "github-webhooks": { "at": "2026-09-14", "fingerprint": "sha256 over that skill's own true signals" }
  }
}
  • match is strong (a declared fit matched), weak (a reader suggested it from its description and said so) or general (fits nearly any repository).
  • required is true for strong matches only. The check warns about a required skill that is not installed, and says nothing about the others.
  • scope records where the skill is installed, or where the plan would install it: project for a committed placement, global for a local one.
  • evidence is what the session-start check compares: for each skill that was in the catalogue at scan time, the repository signals it was evaluated on and the ones that were true. The check compares only signals that both the profile and the installed fit define, and that it could read this time — so a skill added to the catalogue, or a fit.json that edits a signal, never reads as this repository changing. A profile from 1.0.0 has no evidence and gets no evidence check until a refresh writes one.
  • fingerprint is the same information as one hash, kept for profiles written by 1.0.0.
  • declined remembers a "don't ask again" (apply --decline <names>) against the fingerprint of that skill's own true signals, so it is offered again if and only if its evidence changes.
  • A skill the profile lists is never removed by a scan. One whose evidence is gone stays, shown as a - row, until apply --drop <names>; a weak one stays whether or not --weak is passed again.
  • Committed evidence carries counts and repository-relative paths only. Never an absolute path, never a file's contents, never anything read out of session history beyond a number.

The lock file beside it — skills-lock.json — belongs to the Skills CLI and says what is installed. That is why the profile is a separate file: the CLI rewrites its lock, and anything added to it can be dropped without warning.

The routing file

<!-- Generated by onboard-project from skills-profile.json. Do not edit; change the profile and run refresh. -->
# Skill routing for this project

These skills were chosen from evidence in this repository. Load one when the moment matches;
nothing here is enforced.

- about to cut a release, bump a version, or write a CHANGELOG entry → `release-notes`
- delegating to agents, or ending a turn with work still running in the background → `report-progress`

A rules file with no paths frontmatter loads at the start of every session at the same priority as the project CLAUDE.md. That is the entire mechanism this skill relies on: no forcing, no hook, no matcher — the right skills are simply in front of the agent before it goes looking.

It is rendered deterministically from the profile, which is what lets check tell a hand edit from a stale file. Evidence stays in the profile so that the always-loaded text is one line per skill.

The check hook

One entry per new-process matcher (startup and resume), each running:

'<node>' '<skill folder>/scripts/onboard.mjs' check --hook

It is recognised by that command rather than by its describe key, because Claude Code drops describe whenever it rewrites settings.json — an installer that recognises its own work by the describe stops recognising it, and orphaned hooks accumulate. A hook that merely mentions the script is never taken: removing somebody else's hook is the one mistake here with no undo.

It prints zero bytes unless one of three things is true — a required skill is not installed, the repository's evidence has moved, or the routing file no longer matches its profile — and then exactly one line, wrapped in the SessionStart additionalContext envelope. It reads no session history, never blocks, and fails open: any error at all produces no output rather than a failed session start.

It lives in the user's settings, so it applies to every project on this machine. That is why it is always its own row in the change list, always marked, and never installed by this skill on the user's behalf.