Releases: kaisers-io/refs
Release list
v0.12.0
Upgrading
Run refs sync after updating. Two things are worth expecting on that first run.
Packages may be reported as missing that are still on disk. Negated workspace patterns are
applied now, so a package a repository excludes (!examples/vue/2*) is no longer a workspace
member — and an entry registered for one before this release no longer verifies. Nothing is removed
automatically; the finding names the entry and leaves the decision alone.
Packages that arrived upstream since the last sync are reported. Only those: a package the
configuration never had and that did not arrive in the fetched range stays unmentioned, however
long it has been there.
Added
-
refs syncnow reports a package that arrived upstream, andrefs edit --createregisters
it. The drift probe checked the packages the configuration already had, so a package added
upstream afterrefs addstayed invisible — and there was no command to register one either:
refs addrefuses an already-tracked ref, and everyrefs editmode needs an entry to edit. The
only instruction anyone could give was "hand-editconfig.toml".refs syncanswers "did upstream gain a package?" from the range it just fetched, not by
comparing a scan against the configuration. That distinction is the whole design: a scan cannot
tell a package that just arrived from one the ref's owner deliberately never tracked, because
there is no inventory of what was there before — the fetch range is that inventory. A ref whose
owner tracks 3 packages out of 140 hears about the other 137 exactly never.refs doctorlists
every unregistered member instead, because it was asked to.The question it asks of that range is about package NAMES, not manifest paths. A package
renamed in place modifies its manifest rather than adding one, and a package merely moved to
another directory adds one without being new — so a path-based reading is wrong in both
directions. Only the manifests the range actually changed have to be read out of history: an
untouched manifest is byte-identical at both ends, so the name it carries now is the name it
carried before.The repair is a command now rather than a config fragment, with the ref key filled in so it
runs as printed:refs edit 'github.com/acme/alpha' --package '@acme/new' --create --path 'packages/new' \ --description "<what it is>"It is a distinct mode, not an upsert — an ordinary field edit naming an unregistered package
still fails withnot_found, so a typo in--packagecan never become a new entry. The
finding carriesnameandpath, both verified against the checkout and both shell-quoted
(being verified makes a value true, not shell-safe:zPackagePathpermits$()and a manifest
nameis checked only for being non-empty), and deliberately no description: a manifest description is untrusted third-party content, and copying it moves it
into a file refs later reads as its own configuration. The skill instructs agents to propose the
registration and wait for the user to agree, rather than run it on their own initiative.
Fixed
-
Workspace patterns are matched by
minimatch, the matcher npm itself uses. Hand-written
matching disagreed with the real resolvers in five distinct ways, each found only after the last
was fixed: extglob (@(a|b)) read as a literal directory name, trailing slashes treated
symmetrically whereminimatchis asymmetric, repeated separators silently matching nothing, and
two more. Delegating removes that class of defect rather than the current instance of it.Measured before choosing:
picomatchdisagrees withminimatchon ten of 154 comparisons over
the shapes this scanner supports, exactly on trailing slashes and repeated separators — so it is
not a drop-in. pnpm matches throughpicomatchbut normalizes first, and was measured to agree
withminimatchon every one of those shapes, so one matcher covers both ecosystems.Walking stays here: containment guards, the diagnostics that say why a scan came up short, and
the deliberate one-level depth policy are unchanged.minimatchanswers only whether a path
matches a pattern. Nothing new is installed byrefsusers — the CLI publishes a bundle with no
dependencies — and that bundle grows by 24 KB.One behaviour improves as a consequence: a negation in a shape this scanner cannot WALK
(!packages/{a,b}) is now applied, because applying an exclusion needs matching and never
walking. It used to be reported as unsupported and silently ignored. -
Negated workspace patterns are applied instead of ignored.
!packages/fixtureswas dropped
as an unsupported shape (a v1 simplification), sorefs addregistered packages the repository
had explicitly excluded, and every finding about a repository declaring one was silenced —
TanStack Query declares two, and all hundred of its packages came backunverifiable. Negations
are now expanded exactly like inclusive patterns and subtracted from the result, which makes the
scan an accurate statement of membership rather than an approximation of one.A wildcard inside the last segment (
examples/vue/2*) is supported too, since that is the shape
real repositories exclude by, and a negation nobody can expand costs every finding about the
repository rather than just the paths it names. Glob syntax that is still unimplemented —
{a,b},?,[…]— now reportsunsupported_patternrather than reading as a literal
directory name and silently matching nothing. -
A monorepo can now be resolved by the name in its own root manifest. Workspace detection
expands the globs a repository declares, and a workspace root is not one of its own targets — so a
root that names itself was registered nowhere, andrefs resolve @acme/toolkitcame back empty
for a repository that was tracked all along.refs addnow registers a named root atpath: "."
alongside the workspace members.Both pnpm and Yarn address a workspace root by that name (
pnpm --filter <root-name>,
yarn workspace <root-name>); npm and Turborepo use a positional handle instead. Of eighteen
well-known monorepos surveyed, eight carry a name someone would plausibly use for the repository
and ten carry a throwaway likerootormonorepo-root— which is what settles it: registering
the name costs nothing where it is a throwaway, since nobody resolves"root", and answers the
question where it is not.Two things this deliberately does not do. A repository that declares no workspaces is untouched:
refs add's npm fallback owns that shape, and probing the root there would displace a locator it
did not choose — as it also would where a workspace declaration selects nothing, so the package
named in annpm:<pkg>source survives there too. And where a workspace member already claims the
root's name —@remix-run/react-routeris a real example, in a repository that also publishes
react-router— the member wins and the root is simply not registered, which costs that
repository nothing it had before. That rule lives in detection itself rather than inrefs add,
so relocation agrees with registration: a member that moves is still found uniquely, instead of
becoming ambiguous against a same-named root and leavingresolvewith no path for a package
that is plainly there. And a root is never reported as a package's new location: its name is an
alias for the repository, so a member that upstream deletes is reported as gone rather than as
having moved to the repository root — which would have sent a caller to the wrong directory and
described a move that never happened.Refs tracked before this change keep the package map they were given, and no command adds one
entry to an existing ref —refs addrefuses a tracked ref,refs edit --packageneeds an entry
to edit. Sorefs syncandrefs doctor'sconfig-driftcheck now report a root the
configuration does not register, with the entry to add — including the path registration would
actually use, which is the member's rather than the root's where a workspace member declares the
same name. That costs one manifest read per ref, and a workspace scan only where there is
something to report; it is asked only of refs that already register packages, since a plain
reference repository registers none on purpose and is left alone.The root package takes the ref's own description when its manifest carries none, which is the
ordinary case for a private workspace root. That is not the per-package fallbackrefs add
otherwise refuses: the root is not a package beside the repository, it is that repository. -
A failed lookup no longer reads as an absent repository.
refs resolveexits4when a query
matches nothing, and the message ended "run refs list, or add it:refs add <url>". That second
half is a guess: a query can miss every route while the repository is tracked perfectly well under
another identifier — a monorepo root whose own package name was never registered, for instance.
An agent read the suggestion as confirmation and told someone a repository they had tracked was
not tracked, then stopped.The message now states what was searched and points at evidence rather than prescribing a fix, and
--jsoncarries areasononresolve's routing misses:unmatched_query(nothing matched, by
any route),package_not_registered(the ref is tracked and registers no such package), or
ref_not_registered(a canonical git url named an absent ref — the one case where adding it is
the right answer, since only a canonical url establishes which ref was meant). There is deliberately no reason meaning "this repository does not exist", because nothing
refs can observe establishes that; andreasonis absent on every othernot_found, where its
absence mea...
v0.11.0
Changed
-
The skill's version-question flow moved into its own file. Every question about a
dependency's source loadsSKILL.mdandINVESTIGATE.mdin full, and 30 % of
INVESTIGATE.mdwas a block on resolving versions to tags and diffing between them — read
on every plain source question, used on almost none of them. It is nowVERSIONS.md, with a
route of its own inSKILL.md§5 and a one-line pointer at the end ofINVESTIGATE.md, so
a mis-route costs one extra read rather than a wrong answer.Alongside it, a compression pass over what remains: the worker output contract was stated
twice and is now stated once, the five clickable-link rules became one normalization, and a
handful of sentences that repeated something said a few lines earlier are gone. Nothing
behavioural was removed — the capability gate, the trust boundary, the five hard rules and
the worker prompt's own safety rules are untouched, and the measured partial-clone cost model
(which git commands fetch blobs, and thatgit blamefetches one per visited revision) is
refs' own measurement rather than something the term "partial clone" implies, so it stays
verbatim.A plain source question now loads 20 % less. A version question loads about what it did
before, plus one extra file read.
Added
-
refs syncandrefs doctornow report configuration that has fallen behind its upstream.
A configured package path is only a locator, and upstream can delete or move what sits at it.
Until now onlyrefs resolvenoticed, for the one package an agent happened to route to, and it
persisted nothing — so a package deleted upstream could sit wrong in the configuration
indefinitely while every other package in the same checkout went uninspected.Each successful
refs syncresult now carries a nestedstructure: {status, packages}, probed
inside the lock the sync already holds, right after the checkout was updated. Nothing is stored:
the answer is reported and thrown away, so there is no drift state that can itself go stale. A
removal and a relocation are reported as different findings, because they need opposite repairs —
telling an agent to "fix the path" of a package upstream deleted sends it looking for something
that is not there. Human output gains indented lines under the affected ref and stays silent when
everything resolves; the summary counts and exit code are untouched, since a drifted ref synced
perfectly well.Only refs that actually sync are probed, which keeps
--stale-onlya genuine no-op — and is why
refs doctorgains aconfig-driftcheck as the deliberate "check everything now" counterpart.
It takes each ref's lock with a short timeout and reports the ref as busy rather than waiting,
writes nothing, and reportswarnrather thanfail: the configuration has fallen behind,
nothing is broken.refs listdeliberately stays blind — without stored state it would
turn a cheap inventory command into a locking filesystem sweep. -
refs resolveanswers in one call what used to take three. The skill's investigation flow
beganresolve→sync→resolveagain, and the third call was not ceremony: package
verification had described the checkout as it was before the sync, so reusing that answer meant
reporting a path that no longer necessarily held what it claimed.--sync-if-stalefetches (or
clones) only when the ref is stale or its checkout absent, and everything it reports describes the
checkout afterwards. The rule has left the skill and become code.It refuses, rather than syncing, when the checkout is
unmanagedorunverifiable.sync
hard-resets and cleans; running it against a directory whose identity was never established is
how a stray clone loses its history. A failing sync fails the command rather than returning a
success envelope containing a stale path. -
refs resolve --project <dir>reports the version a project has installed. The skill used to
tell the agent to read the project's lockfile by hand, and nothing in refs touched one — so the
deterministic half of every "what changed between my version and a newer one" question was done
by the least deterministic component available, against pnpm's peer-qualified keys, aliases,
overrides and three vendor-specific formats.The answer is read from
node_modules, walking up in Node's own lookup order, and stops at the
first installation slot that exists rather than the first readable manifest — falling through to
an ancestor would report a shadowed install Node would not have loaded. There is deliberately no
lockfile fallback: a lockfile says what should be installed,node_modulessays what is, and
the second is the question.installed.statusisfound,not_materialized,
unsupported_layout(Yarn PnP, detected but never loaded —.pnp.cjsis project code) or
unverifiable. -
refs resolve --ref <ref>scopes a query to one ref's packages. A package name registered by
several refs used to be answered with "use the full ref key" — advice the command could not
honour, because a full-key query routes by ref and comes back withpackage: null. The error
now names a remedy that exists. -
refs doctorgained alockscheck. A held lock used to be invisible: acquisition failed with a
message that named no owner, anddoctorhad no lock check at all — so the one command meant to
answer "is something stuck?" could not see the thing that was stuck. The check lists every entry
in the locks directory with its recorded owner, how long it has been held, and against which
window.A held lock is not a warning by itself: that is what a concurrent
refs synclooks like, so
it reportsokwith the holder listed.warnis reserved for something that will not resolve on
its own — a recorded process that is gone, a lock past its window and still there, metadata that
cannot be read, or something that is not a lock at all occupying a lock name. Like every other
doctorwarning, it does not change the exit code.
Changed
-
refs resolveestablishes that the path it hands back is really this ref's checkout. It used
to report presence from a.gitentry alone, whileaddandsyncboth ran a stronger guard
before mutating — so the one command whose result is read as "the source is here" was the one
that did not check what was there. A manual clone at the derived path, a half-finishedremove,
a restored backup or a symlinked second home all produced a confident answer about the wrong
repository, with no error and no warning.Every reply now carries
checkout: {status, reason?}—managed,missing,unmanagedor
unverifiable— read straight out of.git/configwithout spawning git, so the hot path stays
subprocess-free. The origin URL is never echoed back inreason; it can carry credentials.managedrequires thecore.hooksPathmarker to be this home's hooks directory, not merely
present — the comparisonaddalready makes — so a manual clone that sets it for its own purposes
does not pass. A config git itself would reject (an unterminated quote, an undefined escape, a
line that is neither a section nor an assignment) isunverifiablerather than partially read: a
file git would not accept is not evidence of identity.Package verification is gated on it. A manifest read inside an unrelated checkout can answer
verifiedfor a package that has nothing to do with the query, so anything other thanmanaged
ormissingnow yieldspackage.status: "unverifiable"instead of a confident location.missingis unchanged and still meanscheckout.status === "missing". Callers should branch on
checkout.status, which answers the questionmissingwas often assumed to. -
The "lock is held" error now says who holds it and for how long. It used to read
lock <name> is held — another refs process is running, which left no way to tell a runningsyncfrom
something that crashed 90 seconds ago. It now names the recorded pid, whether that pid is still
present, how long the lock has been held, and when it becomes reclaimable.It deliberately says "recorded pid … is present (identity not verified)" rather than "held by pid
…": onlyESRCHestablishes that a process is gone, so a pid that answers may equally be an
unrelated process that reused the number. And it says "reclaimable", never "released
automatically" — nothing removes a lock in the background; the phrase means the next acquisition
attempt is entitled to take it. -
An operation that ran without the lock it asked for now fails instead of reporting success.
A lock can still be lost while its holder works — a stolen lock is detected by the next renewal,
or by release finding a foreign token. Previously the callback's result was returned as if
nothing had happened. It is now reported as aconflict(exit code 5), because the work ran
without the mutual exclusion it requested and its result is not trustworthy. An operation that
failed on its own keeps precedence: its own error is what the caller sees.
Fixed
-
The
rm -rfcommandsrefs doctorandrefs addsuggest are now quoted. Ref keys derive from
user-supplied urls and permit spaces,$(), backticks, semicolons and quotes, and the refs home
itself routinely sits under a path containing a space — so pasting an unquoted suggestion could
delete several wrong paths and leave the intended one, or execute a command substitution embedded
in a repository name. The form is nowrm -rf -- '<path>';--additionally stops a path
beginning with-from parsing as options. -
A pid in a lock's metadata is now required to be a positive integer within the range
process.killaccepts.0and negative values are
process-group selectors forprocess.kill, so metadat...
v0.10.0
Added
-
refs resolvenow verifies that the package it routes to is actually where the config says
it is. A configuredpathis only a locator; the package name is its identity, and upstream
repos restructure on their own schedule. Previously a package that had moved — or a different
package that had taken over its directory — was handed back regardless, so an agent read the
wrong source and answered confidently. That failure produced no error and no warning.package.statusnow reports what was established:verified,relocated(found at exactly
one new path, which is returned in place of the stale one),unmaterialized(no checkout yet),
unverifiable(verification could not complete —reasonsays why),ambiguous(the name
exists at several paths, listed incandidates), ormissing. All six exit0; see
docs/commands.mdfor the full contract.relocatedcorrects the answer for that call only and never writes toconfig.toml. Persist
it withrefs edit <ref> --package <name> path <new-path>.
Changed
-
resolve'spackage.local_pathcan now benull. It isnullformissingand
ambiguous, where no safe location is known. A caller that treated a zero exit as "here is a
usable path" must checkpackage.statusfirst; previously the field was always a string. -
Workspace detection now reports why it found nothing. An unreadable or malformed workspace
declaration, an unreadable manifest, a candidate resolving outside the repo, an unsupported
pattern, a package directory reachable only through a symlink — each used to collapse into the
same empty result, leaving a transient read error indistinguishable from "every package was
removed". Each is now reported, and a scan carrying any of them is treated as possibly
incomplete: it can neither conclude that a package is gone nor that a single sighting of one is
unique.refs addis unaffected — it consumes the same best-effort list it always has.A manifest that reads fine but declares no usable
nameis reported too, but does not make
a scan incomplete: there is demonstrably no resolvable package at that path. Nameless manifests
are common enough (zod's own repository root has none) that treating them as failures would
permanently suppress detection for those repos.One limit is deliberate and worth knowing: a scan only covers what the repo's workspace
declaration points at. A package registered byrefs add's npm fallback — atpath: ".", or
the packument'sdirectory— lives outside that coverage, so if it moves,resolvereports
unverifiablerather than guessing. It never reportsmissingfrom a scan that had nowhere
to look.
v0.9.0
Added
-
refs tells you when a newer version is published.
refs syncandrefs doctorask npm at most
once a day and cache the answer;refs syncmentions a newer release in itswarnings, and
refs doctorreports it as acli-updatecheck.refs --versionis untouched — it stays exactly
one version line on stdout, because the skill's capability gate and any script parse it.Both switches live in
[updates]inconfig.tomland default to on:checkgoverns the registry
request everywhere,notifythe routine path only.notify = falsewithcheck = trueis
"don't interrupt me, but answer when I ask" —refs syncneither asks nor mentions,refs doctor
still does both.REFS_UPDATE_CHECKoverridescheck(0off,1on), and the check is off in
CI. The table is absent from a config that wants the defaults, and refs never writes one.Nothing about it is load-bearing: an unreachable registry, a malformed answer or an unwritable
cache all mean "we don't know" and are never reported as a fault. The registry host is hardcoded
rather than read from npm configuration, only a plainx.y.zis accepted from the response, and
the update command is printed for you to run — refs does not install itself.
Fixed
-
A ref can be recorded without a
tag_format. Finalizing an add used to reject a proposal whose
tag_format_candidatewasnull, which left one option for a repository that publishes no tags:
invent a convention. A real user hit this and was asked to confirmv{version}for two
repositories that have no tags at all — a claim nobody had verified, written intoconfig.toml
where later agents read it as fact. The candidate now survives finalize as an absent field.refs tagis the only command that reads it, and it exits3(validation) when there is none,
naming the ref — or the package, with the--packageform of the fix. The distinction from4
carries information:3means this ref cannot resolve any version,4means this particular
version was never tagged. The skill's add flow gained an explicit branch for thenullcase, so
an agent reports the absence instead of proposing something to fill the gap.A format already recorded can only be removed by editing
config.tomldirectly;refs editcan
set one but has no way to unset it.
v0.8.3
Added
- The skill states a trust boundary. Everything inside a managed checkout is untrusted third-party
content — README, comments, commit messages, and anyAGENTS.mdorCLAUDE.mda tracked repo
ships — so it is evidence to read and never instruction to follow. Documentation stays evidence,
including a repo's own contributor and agent docs; what goes to the user as a finding is content
targeting the agent that reads it. Both worker flows carry the rule into their prompts. This
narrows the blast radius of indirect prompt injection; it is not a sandbox, and SECURITY.md says
so.
Changed
-
The skill installs nothing. Its capability gate used to ask permission and then run
npm i -g @kaisers-io/refsitself; it now prints the command — pinned to the version the skill
was written against, not@latest— and stops until the user has run it. A skill that installs
the executable giving it its capabilities is a bootstrap trust boundary worth keeping explicit,
and Anthropic's skill documentation discourages global installs from a skill. -
The
--jsonexamples in the skill's command reference use placeholder repositories
(example-org/…) instead of real third-party ones, and say up front that they are illustrative
output rather than repositories the skill fetches.
v0.8.2
Fixed
-
Documentation that described flows which did not work. The README's quickstart failed in both
of its branches, the skill's onboarding handed the user a prompt naming a zod version that was
never tagged, anddocs/commands.mdshowed a stored url and a package count the CLI does not
produce. Every documented command was re-run and corrected against its real output, and the
skillcheck's Windows note was still describing the gap 0.8.1 closed. -
git_transportwas documented as overridable per ref. It can be written there, since the
override schema is derived from the settings schema, but nothing reads it: onlyrefs add
consults the setting, andaddrefuses a key that is already configured. Documented as inert.
Changed
-
The README and the package page are rewritten around the agent workflow, which is how refs is
meant to be used, with the manual CLI route kept as the side note it is. -
The package description now matches the repository's.
v0.8.1
Fixed
-
Locked commands could hang instead of timing out. If a lock looked abandoned but could not
actually be reclaimed — another process holding the steal claim, or Windows refusing to remove
the directory while a handle was still open inside it — the acquire loop retried without ever
consulting its deadline, so the ten-second acquisition budget never applied and the command spun
until interrupted. Both unbounded paths now honour the deadline and fail with the conflict error
(exit code 5) as documented. -
refs doctorreported a correctly installed skill as missing on native Windows. Its three
global search locations were derived from$HOME, which Windows typically leaves unset, while
the installer resolvesos.homedir()— so all three silently dropped out of the search and the
check reportedwarn, "not found in the locations this check knows about". It now reads the
same home directory the installer writes to. macOS and Linux were unaffected, the two agreeing
there.
Security
-
Canonicalizing a git url no longer takes quadratic time. Trailing slashes were trimmed with a
pattern anchored at the end of the string, which backtracks through a run of slashes from every
position; a url carrying a long run in the middle of its path took 14 seconds to be rejected.
Such a url is reachable —refs add npm:<package>readsrepository.urlstraight out of the
registry's packument, and nothing bounds its length — so a published package could stall the
command that adds it. Trimming is now linear. -
Every git invocation that receives a url now ends option parsing with
--first. Without it,
git honours a url shaped like--upload-pack=<command>and executes it. Urls accepted through
refs addwere already refused by canonicalization, butrefs syncre-reads them from the
config file, where they are only checked for being non-empty — the guarantee therefore held one
step away from the call that depended on it. It now holds at the call.
Changed
-
Published packages carry a provenance attestation.
npm produces these automatically for public repositories, and0.8.0shipped without one
because the repository was private at the time and the check for that fails silently. The
release workflow now states--provenanceand refuses to publish if the repository is not
public. -
refs init's skill-install hint now presents the second form as installing from a local
clone, rather than as a workaround for the repository's development phase. Both commands
are unchanged; only the wording differs.
v0.8.0
Changed
- Lowered the supported Node.js floor from
>=24.12to>=24.2. The real requirement was
alwaysimport.meta.main(used by the CLI's entry-point check), which Node added in 24.2.0 —
the higher number had no other reason behind it. Users on Node 24.2 through 24.11 were
previously blocked for no reason and can now runrefsas-is.
Fixed
- The agent skill's citation contract now binds every source reference, not only
worker-relayed ones. An inline investigation (no worker dispatched, per the step 3
dosing rule) previously fell outside the contract and could emit bare absolute paths
instead of clickable relative-text links.
Removed
-
The Claude Code and Codex plugin packaging (
.claude-plugin/,.codex-plugin/, and the
.agents/plugins/marketplace.jsonmirror). Neither manifest carried a payload beyond the
refsskill, so installing one produced a second copy that could drift from the one
skills addinstalls — observed in practice: tworefsentries in the Codex skill picker,
and an icon that reached only one of them.refsnow ships exactly two ways: the CLI from
npm (@kaisers-io/refs) and the skill from git (skills add).If you installed the plugin, uninstall it (Claude Code:
/plugin; Codex CLI:/plugins)
and install the skill instead — see the install section in the README, which covers the
repository still being private. The@refsplugin-mention gap in Codex (codex-cli 0.146.0,
openai/codex#22078) no longer applies: there
is no plugin to mention. Invoke the skill with/refsin Claude Code or$refsin Codex.
Published retroactively on 2026-08-10, the day this repository became public. The text above is this version's CHANGELOG.md entry, unedited — including anything it said while the repository was still private.
v0.7.0
Changed
refs list,show,resolveandinitprint onekey: valueper line, with a blank line
betweenlistentries. Thekey descriptionheader line is gone, andlocal_path:is now
path:(package path:for the package inside a ref).[stale]/[missing]markers are replaced bysynced: <when>, plusstatus: staleand
missing: …lines when they apply. Human output only;--jsonkeepsstaleandmissing.refs initsaysconfig: unchangedwhere it used to print(noop).- The npm-facing
packages/cli/README.mdnow documents the skill-check search locations the
root README has described since 0.6.1.
Added
last_fetched_atonrefs listitems andrefs resolveoutput,missingandstaleon
refs show— all--json, all additive.- Source citations from the agent skill are now clickable markdown links.
Published retroactively on 2026-08-10, the day this repository became public. The text above is this version's CHANGELOG.md entry, unedited — including anything it said while the repository was still private.
v0.6.1
Fixed
refs doctor'sskillcheck no longer reports a skill that is installed and working as
missing. It only ever looked in~/.claude/skills/refsand~/.codex/skills/refs, but
npx skills add kaisers-io/refs— the documented installer — writes neither: it keeps one
real copy in a shared.agents/skills/refsdirectory and symlinks each agent's own
directory at it. A Claude Code user was rescued by that symlink; anyone without one — a
Codex-only user, or anyone whose install went to the current project rather than$HOME—
was told to install a skill they already had, and never saw the version comparison that
0.6.0made the point of this check. Five locations are now checked, in this order:
~/.agents/skills/refs/SKILL.md(shared ~/.agents),$CLAUDE_CONFIG_DIRor~/.claude
(Claude Code),$CODEX_HOMEor~/.codex(Codex),<cwd>/.agents/skills/refs
(project ./.agents), and<cwd>/.claude/skills/refs(project ./.claude). The last two
are there becauseskills addinstalls into the current project unless-gis passed, and
implies-ywhen it runs inside an agent, so an agent-driven install never touches$HOME
at all — and because naming a single agent (skills add … -a claude-code) switches the
installer to copy mode, which skips the shared.agentsdirectory entirely and writes only
that agent's own. Unlike its global counterpart, the project path takes no env override:
the installer hardcodes a relative.claude/skillsthere. Codex needs no counterpart at
all, being a universal agent whose project install lands in./.agentsin every mode. The
locations are deduplicated by resolved real path, so the usual symlinked install is
reported once rather than once per agent, while two genuinely independent copies are both
compared and a problem in either wins.refs doctor's "skill not found" message no longer claims the skill is not installed, and
no longer names directories it did not search. That list of locations is best-effort and
cannot be otherwise: the paths are theskillsinstaller's implementation detail rather
than a documented contract, the canonical directory has moved before, and 74 agents carry a
global skills directory of their own. A skill installed for some other agent still works
and is simply invisible here, so thedetailnow names the locations it searched and keeps
the install hint for the case where the skill really is missing. Those names are derived
from the paths actually resolved, so with$CLAUDE_CONFIG_DIRor$CODEX_HOMEset the
message names the override rather than the~/.claude/~/.codexit replaced — an override
moves the search, it does not widen it, and pointing anyone at the directory the check just
skipped would be worse than saying nothing. It stays awarn, never afail— the skill's
own capability gate comparesrefs --versionagainst the pin in the file the agent already
loaded and depends on none of this.
Published retroactively on 2026-08-10, the day this repository became public. The text above is this version's CHANGELOG.md entry, unedited — including anything it said while the repository was still private.