@alessandroraffa/tangyr
v6.0.1
Published
CLI for the Tangyr discipline — install and manage operating kits for AI coding tools
Readme
@alessandroraffa/tangyr
CLI for the Tangyr discipline — install and manage operating kits for AI coding tools.
An operating kit is a self-contained package of agents, skills, commands, rules, and templates that implements the Tangyr discipline for a specific AI coding context. The CLI places kit artifacts into the environments consumed by supported tools.
Requirements
Node.js >=22. Running tangyr on an older Node.js version prints a clear error to stderr and exits without attempting to run any command.
Install / run
npx @alessandroraffa/tangyr <command>Or install globally:
npm install -g @alessandroraffa/tangyr
tangyr <command>Commands
Kit management
| Command | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| tangyr install --kit <name> | Install a kit resolved from configured search paths (kitSearchPaths/kit in tangyr.config.yaml) |
| tangyr install --kit-path <path> | Install a kit from an explicit kit root path |
| tangyr uninstall | Remove Tangyr from the target scope |
| tangyr sync | Synchronize installed kit to the latest version |
| tangyr verify | Check installation integrity against the manifest |
| tangyr cleanup | Remove managed artifacts; delegates to uninstall if manifest exists |
| tangyr assess | Pre-installation conflict analysis |
| tangyr list | Display kits discovered in configured search paths and any installed manifests |
Environment
| Command | Description |
| --------------- | -------------------------------------------------- |
| tangyr detect | Detect supported AI tool runtimes |
| tangyr status | Show managed artifact status per tool |
| tangyr doctor | Run environment diagnostics with optional auto-fix |
| tangyr probe | Probe configured platform paths |
Configuration
| Command | Description |
| ----------------- | ----------------------------------------------------- |
| tangyr init | Create a tangyr.config.yaml interactively |
| tangyr config | Display or edit Tangyr configuration |
| tangyr validate | Validate the kit against kitFormat 1 conformity rules |
Command options
Options declared on individual commands in addition to the global flags below. --offline, --refresh, and install/sync's narrow --json are documented in Remote kit source; --scope in Scope.
| Command | Option | Description |
| -------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| install, sync | --loss-report <path> | Write an extra copy of this run's semantic loss report to <path>. The report itself is scope state and is rewritten at <scope>/loss.json by every install and sync regardless, whether or not the run exits 3. |
| install, sync | --ignore-errors | Suppress exit code 3 when the loss report contains error-severity entries |
| install, sync | --kit <name> | Kit to install/synchronize: <name> for a local source, <name>@<version> for a remote one |
| install, uninstall, assess | --tools <list> | Comma-separated list of target tools |
| sync, status, verify, doctor, cleanup, probe | --target <tool> | Limit the command to a single target |
| sync | --component <name> | Limit sync to one component type: instructions, agents, skills, commands, rules, hooks, mcp, settings, output-styles, lsp |
| sync | --force | Rewrite managed artifacts even when provenance matches |
| doctor | --fix | Apply safe automated fixes |
| cleanup | --all | Include all enabled targets |
| config | --edit | Interactively edit mutable config fields |
| auth login | --skip-validation | Store the credential without contacting the configured origin |
| kit update | --kit <name>@<version> | Remote kit reference to re-fetch into the cache |
| cache clear | [target] | Positional: kit name or <name>@<version>; omit to clear the whole cache |
| assess, detect, list, probe, validate | --json | Replace the command's output with JSON |
Authentication
| Command | Description |
| -------------------- | --------------------------------------------------------------- |
| tangyr auth login | Store a client ID/secret credential pair, validating by default |
| tangyr auth status | Show current credential validation state |
| tangyr auth logout | Remove the stored credential |
Resolved environment-first (TANGYR_ACCESS_CLIENT_ID/TANGYR_ACCESS_CLIENT_SECRET), then from the stored file, then interactively.
Downgrading. The credential file (~/.tangyr/credentials.json) uses the client ID/secret pair format. To revert to a CLI predating this format, delete that file and run tangyr auth login again on the older version — the older CLI's single-key reader rejects, rather than misreads, the newer pair-format file, so no silent corruption occurs in either direction.
Remote kit source
A kit can be installed from a signed remote origin instead of a local
directory. The subsections below cover configuration, the cache layout,
offline/refresh/freshness behavior, and the maintenance commands; see
Exit codes below for the codes a remote
install/sync/kit update can reach.
In brief. Once installed, install/sync resolve a warm cache entry
with no network call for 24 hours after the last successful
authorization check (--refresh forces a re-fetch and re-verify;
--offline forces cache-only resolution and fails if nothing is
cached). tangyr kit update refreshes the cache without running the
install/sync pipeline; tangyr cache clear removes cached kits. See
Offline, refresh, and authorization freshness
and Maintenance commands below for the full
behavior, and Authentication above for the
credential-store downgrade path.
Quickstart
# 1. Authenticate — stores a validated credential pair.
export TANGYR_ACCESS_CLIENT_ID=... TANGYR_ACCESS_CLIENT_SECRET=...
tangyr auth login
# 2. Point the config at the remote origin (tangyr.config.yaml).
# source:
# type: remote
# url: https://kits.example.com
# 3. Install by name. A bare name resolves through the channel pointer;
# pin a version instead when you want an exact, immutable one.
tangyr install --kit acme-kit
tangyr install --kit [email protected]sync --kit [email protected] follows the same form. Both commands fetch,
verify the publisher signature and per-file content hashes, materialize
into the local cache, and hand the result to the unchanged local install
pipeline.
Remote source configuration
Set the source scheme in tangyr.config.yaml. All three keys are optional and additive — a config that omits them behaves exactly as before.
source:
type: remote # "local" (default) or "remote"
url: https://kits.example.com # required when type is remote
channel: stable # channel a versionless --kit resolves through; default "stable"type defaults to local when absent. When it is remote, the local source.* path keys are ignored at resolution; they are retained rather than stripped, so switching back to local needs no restoration. A type that is neither local nor remote is rejected at load rather than silently treated as local, and type: remote with no url is rejected as well.
A remote kit is referenced on both commands either by name and explicit version, or by name alone:
tangyr install --kit [email protected] # exact, immutable
tangyr install --kit acme-kit # resolves through the channel pointer
tangyr sync --kit [email protected]An explicit <name>@<version> is immutable and never consults a channel. A bare <name> resolves through the signed channel pointer named by source.channel, defaulting to stable; source.channel must be lowercase alphanumeric with hyphens, 1-32 characters, and is rejected at config load rather than mid-resolution. Passing a bare URL as --kit is rejected with a message pointing at source.url.
A channel pointer is served no-store and is never cached, because "the newest version as of some earlier run" is not what a channel means. Two paths therefore answer from the last version this machine saw, and both say so: --offline, which has no other option and exits 8 if the channel has never been resolved here, and a genuine network failure, which falls back and reports the run as degraded. A pointer that is rolled back, expired, or signed for another channel does not fall back — it fails with exit 7, because proceeding on a remembered version would convert a detected downgrade into a silent one.
| Variable | Default | Purpose |
| ------------------ | ------------------ | --------------------------------------------------------------------- |
| TANGYR_CACHE_DIR | ~/.tangyr/cache/ | Root of the local materialized-kit cache. A leading ~/ is expanded. |
Cache layout
A materialized kit is published under the cache root at
<cacheRoot>/<name>/<version>/. The presence of a .tangyr-complete marker
file inside that directory is what makes the entry complete; an entry
without one is a partial write from an interrupted materialization and is
never read. A .staging/ sibling directory under the cache root holds
in-progress materializations before they are published — it is not itself
a cache entry, and is safe to remove between runs. A cache entry also
carries .tangyr-bundle.json, the signed bundle (manifest, files,
signature) that produced it, written immediately before the completion
marker and read back on a warm-cache use to re-verify the publisher
signature without a network fetch; a bundle that declares a file at either
reserved filename is rejected before any write.
Publishing is atomic: a materialization is verified and written into a
unique temporary directory under .staging/, then made visible with a
single directory rename into <name>/<version>/, with the completion
marker written last. Two concurrent materializations of the same
<name>@<version> stage independently and cannot corrupt each other or
expose a half-written kit.
A warm, currently-valid entry that is also freshness-fresh (within the
24-hour authorization-freshness window from the last successful check for
that origin) resolves fully offline, with no network call at all. Beyond
the window, the next install/sync performs a lightweight authorization
re-check against the origin.
A definitive denial fails closed with no cache fallback; a genuine network
failure during that re-check falls back to the warm cache with a warning
and, under --json, a {"degraded":true} signal. See
Offline, refresh, and authorization freshness
below.
The cache root also carries a .tangyr-freshness.json file, recording, per
configured origin, the timestamp of the last successful authorization
check — used by the cache-first freshness gate. This file is not itself a
cache entry: it is excluded from
tangyr cache clear.
Limits and file modes
A materialized bundle is bounded on two axes: the total decompressed payload may not exceed 50 MiB, and no single file inside it may exceed 5 MiB. The total ceiling is enforced while the response is streamed — the request is aborted the moment the running total would cross it, before the payload is buffered — so an oversized or maliciously compressed response is cut off mid-transfer rather than fully downloaded first. The per-file ceiling is checked after each file is decoded and hashed.
Materialized files are written at 0600 inside directories created at
0700. No symbolic or hard links are ever created by the materialization
pipeline, and a pre-existing symbolic link at a write target is refused
rather than followed.
Credential-precedence trap: setting exactly one of
TANGYR_ACCESS_CLIENT_ID / TANGYR_ACCESS_CLIENT_SECRET fails every remote
install and sync with exit 5 — even when a valid stored credential
from tangyr auth login exists. A leftover shell export, a half-applied
.env, or an inherited CI variable is enough to trigger it, because the
partial environment is treated as a misconfiguration and never falls
through to the stored credential. The fix: unset the stray variable, or set
both.
CI and non-interactive operation
install/sync on a remote source resolve the credential from
TANGYR_ACCESS_CLIENT_ID/TANGYR_ACCESS_CLIENT_SECRET or the stored
credential file only — this path never prompts, with or without the
global --non-interactive flag, so a CI pipeline supplying both
environment variables never hits a prompt on the credential-resolution
step. auth login additionally supports --non-interactive explicitly:
under it, a credential that cannot be resolved fails closed naming both
environment variables rather than prompting. The cache root is stable
and documented (~/.tangyr/cache/ by default,
TANGYR_CACHE_DIR-overridable — see
Remote source configuration
above), so a CI pipeline can restore it between runs to keep a warm
cache. A fully non-interactive run with environment credentials against
the documented cache path is exercised end to end by
tests/integration/remote-first-run.test.ts; a real spawned CLI process
reaching a real (fixture) origin non-interactively, with zero prompts,
is additionally exercised by the exit-5 and exit-7 cases in
tests/e2e/cli.test.ts. A full non-interactive success through a real
spawned process against a real signed production origin is not yet
provable — it requires a real, live, signed origin, which does not
exist yet (plan Decision 9's origin-provisioning track).
Offline, refresh, and authorization freshness
The cache root's .tangyr-freshness.json file (Activity 1) records, per
configured origin, the timestamp of the last successful authorization
check. A warm, currently-valid entry resolves fully offline — no network
call at all — for 24 hours from that timestamp. Beyond the window, the
next install/sync performs a lightweight re-check against the origin
before resolving.
--offline forces cache-only resolution: it never fetches and never
mutates. A complete, valid entry resolves immediately; a missing or
unusable entry exits 8 (Offline cache miss) naming the tangyr kit
update remediation. --refresh forces a re-fetch, re-verify, and replace,
bypassing the freshness gate entirely — it always reaches the origin.
--offline and --refresh are mutually exclusive; passing both exits 1.
On a network failure during the lightweight re-check (not a definitive
denial), resolution falls back to the warm cache with a warning printed to
stderr, since the check could not confirm freshness.
Under --json, this same condition also emits a {"degraded":true,"kit":
"<name>@<version>"} line to stderr. Neither install nor sync has a
full JSON output mode yet: --json is scoped to exactly this one signal
and is a no-op when resolution was not degraded — the rest of the
command's human-readable output is unchanged either way.
Migrating a pre-existing cache entry. A cache entry materialized before
this StepLedger (no .tangyr-bundle.json) is not corrupt — it is a
legitimate entry in an earlier format. It is treated as an offline cache
miss (exit 8) specifically under --offline, because there is nothing to
re-verify signature freshness against without a network attempt. Running
tangyr kit update --kit <name>@<version>, or any install/sync without
--offline, while online, upgrades it to the current format.
Maintenance commands
tangyr kit update --kit <name>@<version> is install-free: it re-fetches
and re-verifies the named kit into the cache — replacing whatever is
already there, regardless of the current cache state (missing, stale,
warm-and-fresh, or a pre-existing legacy-format entry) — without running
the install or sync pipeline. It does not affect the authorization-freshness
window: it makes no call to the lightweight authorization probe and neither
reads nor writes .tangyr-freshness.json, since a full re-fetch and
re-verify is a strictly stronger refresh than a freshness check. A later
install/sync still evaluates its own freshness window independently of
any tangyr kit update that ran before it.
tangyr cache clear [target] removes cached kits from the local cache,
in one of three forms, each a distinct blast radius:
tangyr cache clear # every cached kit, every version
tangyr cache clear acme-kit # every version of acme-kit only
tangyr cache clear [email protected] # exactly that one cache entryNo target is the widest and only destructive form: it removes every
<cacheRoot>/<name> directory. A bare <name> removes every version of
that one kit as a single directory. A <name>@<version> removes exactly
one cache entry, the narrowest of the three. None of the three forms ever
removes the cache root's .staging/ directory or its
.tangyr-freshness.json file — both are reserved cache-root filenames,
excluded by name from every form, not only from the no-argument
enumeration.
The command is idempotent: running it again with the same argument, once
its targets are already gone, reports "nothing to remove" and exits 0
rather than throwing or silently reporting a false positive. Without
--yes, it prompts for confirmation, naming the targets it is about to
remove rather than a bare count; --dry-run previews the targets without
removing anything; --yes skips the prompt.
cache clear's [target] is this codebase's first use of Commander's
.argument() for a command's own positional value — distinct from every
.option() flag on this or any other command. It is the established
pattern for a future command that needs exactly one optional positional
value: .argument("[name]", "<description>"), with named flags still
declared via .option(). Commander delivers the positional argument's
value as the .action() callback's first parameter, ahead of any options
object — the reverse of every option-only command's callback signature.
The kit's instructions file
One resolution, used by install, sync, every adapter and verify alike:
source.instructionsintangyr.config.yaml, when set to something other than the./AGENTS.mddefault. An explicit declaration naming a file the kit does not carry is an error, reported before anything is written.entrypointintangyr-kit.yaml.ORIENTATION.md, thenAGENTS.md, then the legacyCLAUDE.md(which installs, with a notice naming the rename).
A kit whose entrypoint is ORIENTATION.md therefore needs no source.instructions, and verify recognises it.
Configuration file
tangyr.config.yaml declares schema: 1. A key this schema does not define is rejected, naming the key it was probably meant to be — an unrecognized key is never read, so whatever it was meant to set stays at its default, and target: for targets: silently reduced an install to one tool. Prefix a key with x- to keep it as an annotation.
In tangyr-kit.yaml, discipline is optional: declare it to opt into the corpus-version check, omit it for a kit that is not derived from the shared corpus.
Conflict policy
Before writing, each destination is classified and resolved according to onConflict in tangyr.config.yaml: ask (default; prompts, and resolves to skip when there is no one to ask), skip, backup, or overwrite.
--yes supplies the confirmation a non-interactive run needs; it does not override the posture. Under --yes, skip leaves conflicting destinations unchanged and says which, overwrite replaces without a backup, ask leaves the destination alone and names the postures that replace, and backup copies the existing file — or the existing directory, recursively — into .tangyr/backups/<timestamp>/ and records it in the manifest so uninstall can put it back. A symlink is backed up as a symlink and restored as one: its target is untouched either way, but the link itself is the operator's artifact — often the one committed with the project — so replacing it without a record left nothing to put back.
One decision is taken per destination, not per target, so a file several targets write — AGENTS.md is claimed by four — is backed up once.
Accepted loss
Some losses are structural: OpenCode expresses hooks only as plugin code, Cline has no committable persona file. A kit with hooks installed for OpenCode therefore exits 3 on every run, however correct the kit is.
acceptedLoss names those, so that a genuinely new error entry is still loud:
acceptedLoss:
- opencode/hook-as-plugin # <target>/<entry-type>
- cline/* # every error entry for one target
- persona-not-representable # this entry type on any target--ignore-errors remains the blunt instrument: it suppresses the exit code for every error entry, including ones nobody has looked at yet.
Not every error entry is structural. rule-not-delivered (below) reports a rule that did not reach a target on this run, which is usually fixable — by giving the target an instructions path of its own, or by not enabling it. Accept it when you have decided to live with it, not to quiet it.
What did not arrive
A kit's artifacts reach a target through that target's own surfaces: a file in its agents directory, a prompt, a workflow, a skill folder, a section in the instructions document it reads. The surfaces differ per target, and some targets have none for a given category.
After every target has written, the run measures what is actually on disk and says so — one line per target, the counts first, and whatever did not arrive named beneath:
What reached each target (measured on disk):
claude-code agents 1/1 commands 1/1 rules 1/1 skills 1/1 hooks 1/1
copilot agents 1/1 commands 1/1 rules 1/1 skills 1/1 hooks 1/1
codex agents 1/1 commands 1/1 rules 0/1 skills 1/1 hooks 1/1
rules missing: git-safety
folded into AGENTS.md, which cline wrote last
cline agents 0/1 commands 1/1 rules 1/1 skills 1/1 hooks 0/1
agents missing: reviewer
cline has no agents surface in this scopeMeasured on the files, not on what the compilers intended: the two have disagreed, and the files are what the target reads. Nothing is printed when everything arrived; --verbose shows the block either way.
The same fact reaches the loss report — <scope>/loss.json, rewritten by every install and sync, plus any --loss-report <path> copy — as a rule-not-delivered, agent-not-delivered, command-not-delivered, skill-not-delivered or hook-not-delivered entry, one per artifact per target, at error severity — so a run that loses part of the kit exits 3 rather than reporting success. A category the target has no surface for at all is left to the structural entry that already says so (persona-not-representable, hook-as-plugin), rather than repeated once per artifact.
The loss report
.tangyr/loss.json (project scope) or ~/.tangyr/loss.json (global) is written by every install and sync, and removed by uninstall with the manifest. It holds one entry per target and component that could not be translated natively, each with its closed-vocabulary type, its severity and a human-readable detail, under a generatedAt timestamp.
It used to be written only when --loss-report <path> was passed, which nobody passes twice: a project found theirs five days stale, naming a component renamed since, while the kit had grown from two components to thirty-four. A report that is only ever a photograph of one run does not belong in a directory that looks like state.
Exit codes
The full exit-code taxonomy, in numeric order. Every code below 5
predates the remote-source work and is shared by every command; codes
5-9 are specific to a remote install/sync/kit update (see
Remote kit source above for the pipeline that
reaches them).
| Exit code | Meaning | Reached when |
| --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 0 | Success | The command completed with no fatal error. A sync/install whose loss report contains only info/warning-severity entries also exits 0. |
| 1 | General failure | The generic, catch-all failure code for any error not mapped to a more specific code below — including a CLI usage error such as an unrecognized command, a malformed --kit <name>@<version> reference, or --offline and --refresh passed together; a corrupt or legacy-format installation manifest (ManifestCorruptError, ManifestLegacyError, src/core/manifest.ts) also falls through to this code, since handleFatalError carries no dedicated branch for either. |
| 2 | Conflict | Reserved for a conflict-resolution failure (ConflictError, src/core/exit.ts); declared and routed by handleFatalError, but not thrown by any current production code path. |
| 3 | Completed with error-severity loss | An install/sync run completes but its loss report contains at least one error-severity entry, distinct from the generic failure code 1. --ignore-errors on install/sync forces a zero exit for authors who accept the loss. |
| 4 | Target not detected | Reserved for a configured target tool whose runtime cannot be detected (TargetNotDetectedError, src/core/exit.ts); declared and routed the same way as exit 2, and likewise not thrown by any current production code path. |
| 5 | Authentication required | The resolved credential is missing, partially configured (see the credential-precedence trap above), or rejected by the origin (401/403) — at login, at a cold fetch, or at the post-freshness-window re-check. |
| 6 | Remote unavailable | A transient fetch failure exhausts the bounded retry (3 attempts, 250 ms then 1000 ms backoff) with no warm cache to fall back to, or a warm-cache freshness re-check fails the same way with no usable fallback path. |
| 7 | Integrity failure | The signed bundle fails signature verification (including an unknown or out-of-window signing-key identifier), identity binding, a per-file content-hash check, a size ceiling, or path sanitization; or the channel pointer for a versionless --kit <name> cannot be resolved, is not signed by a pinned key, or names a version older than the one already seen. |
| 8 | Offline cache miss | --offline with no complete, valid cache entry for the requested reference. |
| 9 | Incompatible version | The running CLI is older than the bundle's declared minimum-compatible version, or the origin rejects this CLI's contract version. |
| 10 | Configuration missing | No tangyr.config.yaml is found (ConfigNotFoundError, src/core/config.ts) for a command that requires one. |
| 130 | User interrupt | The process received SIGINT (Ctrl-C) during a command. |
Global flags
| Flag | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------- |
| --config <path> | Use an explicit configuration file |
| --kit-path <path> | Use an explicit operating kit root |
| --project-root <path> | Directory a project-scope operation writes into (see Scope) |
| --dry-run | Report actions without modifying files |
| --yes | Skip confirmation prompts |
| --non-interactive | Never prompt; fail closed when a required input is missing (auth login) |
| --verbose | Detailed output |
| --quiet | Errors only |
| --no-color | Disable colors |
| --claude-config-dir <path> | Claude Code global root directory (repeatable). Overrides claudeCodeGlobalPaths in the config file. |
Supported tools
- Claude Code — instructions (
AGENTS.mdplus theCLAUDE.mdshim), agents, skills, commands, rules, output styles, hooks, settings, and MCP, placed in scope-resolved Claude Code paths (fanned out across every configured global root — see Multiple Claude Code global roots) - GitHub Copilot — instructions (
AGENTS.mdplus the always-read.github/copilot-instructions.mdmirror; project scope only, since Copilot has no global instructions slot), agents, prompts (compiled from commands), rules (.github/instructions/<name>.instructions.md, each carrying the rule's path scope as anapplyToglob; project scope only, since that is a repository path with no user-level equivalent), skills, hooks (~/.copilot/hooksor.github/hooks), and MCP. Hook matchers are rewritten to Copilot's own lowercase tool names (bash,view,create,edit,grep) — an untranslatedBashmatcher never fires - OpenAI Codex —
AGENTS.md, agent TOML files, rules (.rulesfiles forexec-policy, appended to the instructions file as guidance otherwise), skills and commands (both as skill directories at Codex's documented skills path —~/.agents/skillsglobally,.agents/skillsinside a repository — because Codex deprecated its custom prompts in favour of skills), hooks (hooks.jsonplus the shared~/.agents/hooksscript bridge; macOS and Linux only), and MCP inconfig.toml - OpenCode — instructions (
~/.config/opencode/AGENTS.mdglobally;.opencode/AGENTS.mdinside a project, declared in theinstructionsfield ofopencode.json, because the project-rootAGENTS.mdis written by four other targets in turn and only the last writer's content survives), agents, commands, skills (OpenCode-local plus the shared~/.agents/skillslink), andopencode.json(MCP, permissions, custom provider), in XDG config paths. Hooks are not delivered: OpenCode exposes hooks only as plugin code, so a declared hook event is dropped and recorded as ahook-as-pluginloss - Cursor — instructions, subagents,
.mdcrules, commands, skills (Cursor-local plus shared),hooks.jsonplus the shared hook-script bridge (matchers rewritten to Cursor's tool names —Shell,Read,Write,Grep),mcp.json, andpermissions.json - Cline — instructions (
AGENTS.md, project scope only), rules, workflows (compiled from commands), skills, and MCP. Agents and hooks are not delivered: Cline has no committable persona file (persona-not-representable) and exposes hooks only as@cline/sdkplugin code (hook-as-plugin); both are recorded as losses rather than silently skipped
Quick start
# Initialize a configuration in your project
npx @alessandroraffa/tangyr init
# Install a kit by path (preview with --dry-run first)
npx @alessandroraffa/tangyr install --kit-path ./path/to/operating-kit --dry-run
npx @alessandroraffa/tangyr install --kit-path ./path/to/operating-kit
# Check installation status, then sync after a kit update
npx @alessandroraffa/tangyr status
npx @alessandroraffa/tangyr syncScope
Every write command accepts --scope global or --scope project. The CLI flag overrides config.scope for that invocation.
| Scope | Manifest location | Claude Code artifacts |
| --------- | ------------------------- | ----------------------------------- |
| global | ~/.tangyr/manifest.json | ~/.claude/ |
| project | .tangyr/manifest.json | repo root (AGENTS.md, .claude/) |
Which directory is the project
A project-scope operation resolves its root in this order:
--project-root <path>, when given.- The directory of the
tangyr.config.yamlthis run discovered — one found in the working directory or an ancestor. That file sits at the project root by construction, andkitPathis already resolved against it. - The working directory, when the config was named explicitly with
-c. An explicit path may name a shared or CI config outside the tree and says nothing about where the project is. - The nearest enclosing directory carrying
tangyr.config.yamlor.tangyr/, for the commands that run without a config at all; failing that, the working directory.
Running from a subdirectory therefore installs at the project root rather than in the subdirectory. Deliberately only Tangyr's own markers are searched in step 4: a package.json or .git above the working directory says where some project starts, not that this installation belongs there.
Multiple Claude Code global roots
If you maintain more than one Claude Code account (e.g. ~/.claude and ~/.claude-work), you can keep them identical with a single sync operation.
Via config (tangyr.config.yaml):
claudeCodeGlobalPaths:
- ~/.claude
- ~/.claude-workVia environment variable (TANGYR_CLAUDE_CONFIG_DIRS):
A colon-separated list of Claude Code global root directories. Entries support ~/ expansion. This layer is designed for machine-specific configuration that must not be committed to a shared kit config.
export TANGYR_CLAUDE_CONFIG_DIRS="~/.claude:~/.claude-work"
tangyr syncAuto-sync use case: if a kit-change hook or automation runs tangyr sync, exporting TANGYR_CLAUDE_CONFIG_DIRS in the shell environment keeps multiple Claude configs in sync automatically — without putting machine-specific paths in a shared tangyr.config.yaml.
Via CLI flag (repeatable):
tangyr sync --claude-config-dir ~/.claude --claude-config-dir ~/.claude-workPrecedence: --claude-config-dir flag > TANGYR_CLAUDE_CONFIG_DIRS env > claudeCodeGlobalPaths in config > default ~/.claude.
Per-root artifacts (instructions, agents, skills, commands, rules, output-styles, hooks settings) are written under each root. Shared artifacts (~/.claude.json for MCP, ~/.agents/hooks for hooks) are resolved from the platform default and written once regardless of how many roots are configured.
The manifest records the full set of roots written so that verify and uninstall operate on all of them.
