Guide

Registry and paths

How resolve expands a template, what a platform tag hides, and why some paths carry a note.

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:

ts
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.

ts
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:

md
---
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.

ts
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-template can add other paths.
  • OMP source: Markdown in .omp/prompts/ and ~/.omp/agent/prompts/, including subdirectories. Markdown files under the separate commands/ 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/. The prompt field contains the template; {{args}} receives arguments.
  • OpenCode: Markdown in .opencode/commands/ and ~/.config/opencode/commands/. The config's command map is another way to define a template. $ARGUMENTS and 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_HOME can 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.

ts
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.

ts
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

ScopeMeans
userSettings of one user, under the home directory
projectInside a repository, relative to its root
systemFor the whole machine, usually managed by an administrator
dataTranscripts, 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

ts
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

shell
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.