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, ${TMPDIR} the system temp directory (os.tmpdir(), or the tempDir option), %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.
Prompt templates
promptTemplates is a PathCandidate[] for reusable prompt files. It isn't the
same thing as a system prompt or a skill directory. When a harness implements
templates as custom slash commands, its paths can appear in both commands and
promptTemplates. OMP also has TypeScript commands, so treating every command as
a template would be wrong.
const pi = getHarness("pi");
pi.promptTemplates;
pi.resolve({ homeDir: "/home/dev", platform: "linux" }).promptTemplates;
The registry records default locations, not every directory a user can configure. An empty array means no separate template location has been verified for that CLI, not that reusable instructions are impossible.
One shared format
The synchronization source is $XDG_DATA_HOME/agntn/prompts/, falling back to
~/.local/share/agntn/prompts/. This is durable user-authored data, not
configuration, so it belongs under XDG data. Each top-level .md file is one
template:
---
description: Review the current change
---
Review $ARGUMENTS carefully.
Names start with an ASCII letter or digit and then use only letters, digits, hyphens or underscores. That subset stays a valid command name in every target.
$ARGUMENTS is the portable argument placeholder. For a Markdown harness, the
prompt directory itself becomes a symlink to the canonical directory, so a file
written through one harness is a template in every other one. Gemini receives
generated TOML commands, with $ARGUMENTS converted to {{args}} and
description carried across. The Markdown remains the source of truth.
import { getAllHarnesses, syncPromptTemplates } from "@agntn/harnesses";
const report = syncPromptTemplates(getAllHarnesses(), true);
The second argument enables check mode. The CLI equivalent is
harnesses prompts sync --check. A Markdown target reports one action for the
directory link and an empty templates list. A directory that already holds
its own files is moved whole to
$XDG_DATA_HOME/agntn/diverged/prompts/<harness>/ before it is linked; one that
only holds per-template links from earlier releases is replaced without a
backup. In the Gemini directory, stale generated TOML is deleted and any other
file is backed up to the same place, so no harness keeps templates the others
don't have.
promptTemplateSyncTarget names the one stable user-scope destination used for
each harness. It is separate from promptTemplates, which continues to record
all verified project and user locations.
Verified locations
Checked against upstream documentation and source on September 19, 2026:
- Pi: Markdown in
.pi/prompts/and~/.pi/agent/prompts/, invoked with/name. Default discovery is non-recursive. Packages, settings and--prompt-templatecan add other paths. - OMP source:
Markdown in
.omp/prompts/and~/.omp/agent/prompts/, including subdirectories. Markdown files under the separatecommands/directories are also reusable prompts; TypeScript commands there aren't templates. - Prime Agent source:
Markdown in
.prime/agent/prompts/and~/.prime/agent/prompts/, invoked with/name. Default discovery is non-recursive. - Gemini CLI: TOML in
.gemini/commands/and~/.gemini/commands/. Thepromptfield contains the template;{{args}}receives arguments. - OpenCode: Markdown in
.opencode/commands/and~/.config/opencode/commands/. The config'scommandmap is another way to define a template.$ARGUMENTSand positional arguments are supported. - Claude Code: Markdown in
.claude/commands/and~/.claude/commands/still works, but commands have merged into skills. Prefer skills for new work. - Codex: deprecated Markdown
prompts in
~/.codex/prompts/, invoked with/prompts:name. Only top-level files are loaded; there is no project prompt directory.CODEX_HOMEcan move the user directory. - Mastra Code source:
Markdown in
.mastracode/commands/and~/.mastracode/commands/, plus Claude command directories and the singular.opencode/command/and~/.opencode/command/compatibility paths. Subdirectories form namespaces.
Similar features with different boundaries
Copilot prompt files
live in .github/prompts/*.prompt.md, but GitHub documents them for IDEs, not
Copilot CLI. Antigravity workflows
are likewise marked as an IDE feature. Neither is advertised on the CLI entry.
Cursor's current command documentation
directs users to migrate commands into skills. Its existing skills metadata
remains the supported route here. Grok's command directories are already recorded,
but their template semantics weren't verified in this review; Freebuff has no
verified standalone prompt directory. Both keep an empty promptTemplates list.
Skills
skills lists every directory a harness scans. skillsSyncTarget is the one
user-scope directory syncSkills owns for it, and it is always one of the
skills entries. Copilot and Freebuff have null: the registry has no user
skills directory for them.
The source is $XDG_DATA_HOME/agntn/skills/, falling back to
~/.local/share/agntn/skills/, beside the prompt templates. Each subdirectory
with a SKILL.md is one skill. Unlike prompts, nothing gets translated: every
harness reads the same SKILL.md layout, so each target just becomes a symlink
to the whole folder.
import { getAllHarnesses, syncSkills } from "@agntn/harnesses";
const report = syncSkills(getAllHarnesses(), true);
Check mode again, and harnesses skills sync --check from the CLI. A target
holding only links to single skills in the source is replaced without a backup,
which is the usual handmade setup. Anything else is moved to
$XDG_DATA_HOME/agntn/diverged/skills/<harness>/ before it is linked. The source
may itself be a symlink, say into a dotfiles repo. If it points into a harness
directory, that harness is skipped, because backing the directory up would take
the source with it.
Some harnesses read more than their own directory. OpenCode scans
~/.config/opencode/skills/, ~/.claude/skills/ and ~/.agents/skills/, and
its docs want skill names unique across all
of them, so after a full sync it can see each skill more than once.
Temp and env overrides
temp lists where a harness keeps its own temp files. It matters more than it sounds: Claude Code puts session scratchpads under /tmp/claude-<uid>/, and on most Linux distros /tmp is tmpfs, so every node_modules a session installs there sits in RAM until the next reboot.
envOverrides lists the environment variables a harness reads to move one of its roots: the variable, the default root in path, and the categories that live under that root in relocates.
const claude = getHarness("claude");
claude.envOverrides.map((o) => `${o.variable}: ${o.relocates.join(", ")}`);
// [
// "CLAUDE_CONFIG_DIR: config, sessions, instructions, skills, commands, promptTemplates, hooks",
// "CLAUDE_CODE_TMPDIR: temp", // linux and win32, default ${TMPDIR}
// "CLAUDE_CODE_TMPDIR: temp" // darwin, default /tmp
// ]
// with CLAUDE_CODE_TMPDIR=/var/tmp in the environment
claude.resolve({ platform: "linux" }).temp.map((p) => p.path);
// ["/var/tmp/claude-<uid>/"]
resolve applies an override to temp only, and only one it can see in process.env. Claude Code also reads these variables from the env block of its user settings, which resolve doesn't open, so a value set only there doesn't show up. Inside a Claude Code session that doesn't matter: Claude passes the value on to the processes it starts, MCP servers included. The other categories stay on their defaults for now: moving config would move the files mcp sync and agents sync write, and that deserves its own change.
The list covers what was checked against docs or source: Claude Code, Codex (CODEX_HOME), Pi (PI_CODING_AGENT_DIR, PI_CODING_AGENT_SESSION_DIR) and OMP (PI_CODING_AGENT_DIR). An empty list means nothing verified yet, same as an empty path list.
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.