Registry and paths
Templates
Paths in the registry are templates. ~ at the start and ${HOME} anywhere become the home directory, ${PROJECT_ROOT} the project root, %VAR% the environment variable of that name. resolve expands them and filters by platform in one pass:
import { getHarness } from "@agntn/harnesses";
const gemini = getHarness("gemini");
gemini.resolve({ platform: "win32", homeDir: "C:\\Users\\dev" }).config.map((p) => p.path);
// [
// "C:\\Users\\dev/.gemini/settings.json",
// ".gemini/settings.json",
// "C:\\ProgramData/gemini-cli/settings.json" // %PROGRAMDATA% from the environment
// ]
Without options, platform is process.platform, homeDir is os.homedir(), projectRoot is process.cwd(). An unsupported platform string throws; the three that work are linux, darwin and win32. An unset %VAR% stays as written, which is easy to spot and better than an empty string in the middle of a path.
Relative templates stay relative. .claude/skills/ is meant to be joined with whatever project you are looking at, and resolve does not pretend to know which one.
resolvePathTemplate(template, options) is exported on its own for a path you got from somewhere else.
Platforms
A PathCandidate without platforms applies everywhere. One with platforms: ["darwin"] is dropped by resolve on Linux and Windows. The raw arrays on the harness keep every entry, so a tool that wants to show all of them can.
The tags are only set where paths really differ. OpenCode and Freebuff use ~/.config/... on every platform including Windows, and the registry says so in a note instead of inventing an %APPDATA% variant nobody verified.
Notes
A note is there when the path alone would mislead:
~/.claude/projects/<dash-encoded-cwd>/*.jsonl- the placeholder tells you the directory name is derived, not literal.~/.codex/skills/- deprecated in favour of~/.agents/skills/, kept because installs still have it..claude/skills/under Cursor, Copilot, OpenCode and Mastra Code - those harnesses read Claude's directory natively, which decides where a shared skill has to live.
Read them. A path without its note is how a shared skill ends up in a directory nobody scans.
Scopes
| Scope | Means |
|---|---|
user | Settings of one user, under the home directory |
project | Inside a repository, relative to its root |
system | For the whole machine, usually managed by an administrator |
data | Transcripts, histories and databases the harness writes for itself |
sessions entries are data. Config files under ~/.config are user, even on the harnesses that keep their database there too.
Detection
harness.detectEnv(); // any of detection.envVars set to something
harness.detectProject(cwd); // any of detection.projectMarkers exists under cwd
harness.isInstalled(); // any of binaries found on PATH
harness.version; // first semver in `<binary> --version`, or null
detectHarness(cwd) combines them: env first, then a single project match, else null. detectHarnessFromEnv() and detectProjectHarnesses(cwd) are the two halves, exported separately.
In the CLI
harnesses info claude # the raw registry entries
harnesses paths claude # resolved for this machine
harnesses paths codex --json # same, as JSON
Or in the explorer, for a platform and home directory that isn't yours.