yadgar
OfficialThis server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
yadgar — the client
One binary. yaadgaar login obtains a credential, yaadgaar serve is the MCP
server your agent spawns, and everything else manages the machine it runs on
(D75, D76).
The command is yaadgaar, and the doubled vowels are not a typo. The Python
client this replaces already owns yadgar on PATH, so the transitional client
differs in both the PyPI project and the executable — the reasoning is written
over BINARY_NAME in src/install/hooks.rs.
Decisions are recorded in yadgarhq/docs —
D75 (the proxy that knows no tools), D76 (one binary, and a rules file that is
referenced rather than spliced), D72 (the credential), D16 (why the gateway is
the only thing that speaks MCP inward).
What it is
A proxy, and deliberately nothing more. tools/list is forwarded to the gateway
and its answer returned verbatim; so is tools/call. There is no per-tool code
in this repository at all — not a match arm, not a registry, not a feature
flag.
That is the boundary rather than an economy. A list the client asserts is a list the client is trusted to filter honestly; forwarding makes the gateway's answer the only answer, and the gateway resolves identity before it replies. It is also what keeps releases uncoupled: this binary lives on people's laptops and the gateway does not, so a client that enumerated tools would make the tool set move at the speed of the slowest machine.
When the gateway is unreachable at spawn, the last tool list is served from cache and calls fail with a plain "gateway unreachable" — because an empty list is indistinguishable from yadgar not being installed, and the agent would silently lose its tasks with nothing to report.
Related MCP server: super-mcp
Commands
Command | What it does |
| Asks for the gateway address and your credentials once and stores both together. It registers nothing — |
| Redeems an admin's enrolment token: sets your password, prints your username, and stores the gateway address and credential. Replaces |
| Prints who your saved credential names: your |
| The stdio MCP server the agent spawns. Not run by hand. |
| Registers the hooks, the rules reference and the MCP entry, in the agent client's user-level config — once per machine, never per repository. |
| Removes what |
| Reports drift in the installed environment. Scheduled, not remembered — the daemon cannot see |
| What |
| Issues an enrolment token for an existing user ( |
| Revokes every live credential a user holds, now ( |
| Lists a user's live credentials ( |
| Revokes one credential ( |
enrol (first machine) or login (any other), then install, is the whole
setup, and verify is the one to schedule. The project key is read once, from
the directory the session was started in (D53) — see
Getting started — so one registration serves every
repository.
Unattended provisioning
login and enrol are interactive by default: three prompts each, and
rpassword reads the password straight off the controlling terminal rather
than stdin. A machine with no terminal to answer those prompts — a VM image,
a container build, a config-management run — cannot use either command as
they stand. Both take flags that remove every prompt:
Flag | Commands | What it replaces |
|
| The "Gateway address" prompt. |
|
| The "Username" prompt. |
|
| The concealed password prompt ( |
|
| The positional token argument or the "paste the enrolment token" prompt. |
A fully unattended login:
printf '%s\n' "$PASSWORD" | yaadgaar login --gateway https://gateway.yadgar.internal:18443 \
--username someone --password-stdinAdd --ca-file /path/to/ca.pem when the deployment uses a private CA and this
machine never enrolled. The CA is trusted in addition to the system roots and
stored with the credential.
A fully unattended enrolment:
printf '%s\n' "$NEW_PASSWORD" | yaadgaar enrol --token-file /path/to/token --password-stdinprintf '%s\n', not echo. Under dash (Debian/Ubuntu's /bin/sh) and
some other shells, a bare echo interprets backslash escapes in its
argument by default — a password containing \n, \t, or similar is
silently truncated or rewritten before it ever reaches this client.
printf '%s\n' never interprets its argument at all.
--password-stdin requires --gateway and --username on login.
Without them, the prompts those two flags remove would still read stdin
themselves — consuming the very lines meant for the password. enrol
similarly requires the token as the positional argument or via
--token-file when --password-stdin is given: the interactive "paste the
enrolment token" fallback also reads stdin, and --password-stdin has
already claimed it for the password.
The password is read as exactly one line: the first, with its trailing
line ending stripped. It is never trimmed — a password can legitimately
start, end, or consist of whitespace — and an empty result is refused.
--password-stdin refuses a terminal, unlike docker login, which
accepts one. rpassword is used everywhere else in this client precisely so
a password is never echoed and never reaches scrollback; reading one off a
real terminal through this flag would type it in plain sight instead of
concealing it, which is the exact leak this flag exists to close. Redirect
input from a file or a pipe.
Nothing about the interactive path changes when none of these flags are given.
Resetting a password
An administrator issues a new enrolment token; the person redeems it and chooses the new password. The administrator never learns it.
Find the user's id. A new user gets one from the gateway's
/admin/create-user; the gateway has no lookup by username.Issue the token with your own administrator credential:
yaadgaar admin issue-enrolment --user-id "$USER_ID" --out reset.token --revoke-existing--outwrites a new 0600 file and refuses an existing one. Leave out--revoke-existingto keep the user's other machines logged in.Hand
reset.tokenover out of band. The person runs:yaadgaar enrol --token-file reset.token
Revocation happens at redemption, not at issue. The user's old
credentials keep working until the person runs enrol with the token. The
token is single-use and expires in 24 hours.
Revoking a leaked credential now
To kill a leaked token at once, without waiting for a reset, revoke every credential the user holds:
yaadgaar admin revoke-credentials --user-id "$USER_ID"The id must be the canonical yadgar:user:<lowercase uuidv7>; anything else
is refused before a request is made. The command prints the number of
credentials revoked. A count of 0 prints a warning: the user held nothing
live, or the revocation already happened. It logs the user out of every
machine, and they can log in again, because the password is unchanged and the
account is not locked. It asks no confirmation. The gateway refuses (403) a
non-administrator, the bootstrap token, and an administrator targeting
themselves, and answers 404 for an unknown user.
Revoke-all also voids the user's outstanding enrolment tokens (ADR-0972 (5)).
The command prints that count too, as voided N outstanding enrolments. A
gateway older than gateway#127 does not report it, and then the line is absent.
Revoking one credential
To log out one machine and keep the others, list the user's credentials and revoke one by its id:
yaadgaar admin list-credentials --user-id "$USER_ID"
yaadgaar admin revoke-credential --user-id "$USER_ID" --credential-id "$CREDENTIAL_ID"list-credentials prints one tab-separated row per live credential on stdout:
the id, the creation time, the expiry, and the label the machine gave at login.
The expiry is never for a credential minted before expiry existed. A count
goes to stderr. An unknown user gives an empty list, not an error. A control
character in any field the gateway sends, as in its error text, prints
escaped, as \u{1b}, so it cannot drive the terminal.
revoke-credential prints revoked credential <id>. It refuses a receipt that
names another user or another credential. The gateway answers 404 no such credential for this user for an unknown id and for another user's id alike.
Unlike revoke-credentials, an administrator may revoke their own credential
here (ADR-0972 (2)). Both commands need gateway#127 or later; an older gateway
answers 404 and the command says so.
Finding your private project prefix
A private project path is local/<account_segment>/… (ADR-0954). Ask the
gateway for your segment rather than guessing it:
yaadgaar whoamiuser_id: yadgar:user:<uuid>
account_segment: <uuid>
private prefix: local/<uuid>/
expires_at: 2027-01-19T08:00:00Zexpires_at is the expiry the gateway gave at login or enrol, stored in
the config (ADR-0972 (3)). It is unknown for a config written before this
field, or from a gateway older than gateway#128.
It uses the saved login and calls the gateway's GET /auth/whoami
(ADR-0959, gateway 0.12.17 or later). It exits non-zero when there is no saved
login, when the gateway refuses the credential (401: it is invalid or revoked,
so run yaadgaar login again; after the stored expiry it names the expiry
instead), when the gateway cannot be reached, when the
gateway answers 404 (it is older than 0.12.17 and does not serve the route),
when the gateway names no account segment (the dev trusted-headers path), and
when the segment is not one path segment (it holds a /, whitespace or a
control character).
Credential expiry
A credential from login or enrol expires (ADR-0972 (3); 90 days by
default). Both commands store the expiry the gateway answers, and whoami
shows it. The password does not expire, so the fix is always yaadgaar login.
A 401 after the stored expiry names the expiry and says to run
yaadgaar login. This holds inserve,whoamiand everyadmincommand. Before the expiry, or with no stored expiry, the gateway's own reason is shown as before.A credential minted before expiry existed has none and keeps working.
enrolcan answer that the password is set but no credential was issued, because the account changed during the redemption (HTTP 409, ADR-0977). The password is then set and nothing is stored: runyaadgaar login.
Installing
pipx install yaadgaarpip install yaadgaar installs the same wheel, which holds only the binary.
To move to a newer release, pipx install --force yaadgaar==<version>.
Getting started
Enrol, on the first machine, with the enrolment token an admin gave you.
enrolsets your password, prints your username, and stores the gateway address, the credential and, on a private deployment, the CA to trust — the same configloginwrites. Nologinis needed afterwards.yaadgaar enrol --token-file /path/to/tokenUnattended, with the new password on stdin (see Unattended provisioning):
printf '%s\n' "$NEW_PASSWORD" | yaadgaar enrol --token-file /path/to/token --password-stdinOn any further machine,
yaadgaar loginwith that username and password. For a deployment with a private CA, pass the CA's PEM file with--ca-file /path/to/ca.pem;loginstores it with the credential, asenrolstores the CA its token carries.Install, once per machine. This registers yadgar with Claude Code at user level: three hooks in
~/.claude/settings.json(yaadgaar hook session-startonSessionStart,yaadgaar hook task-updateonPreToolUseforTaskUpdate, andyaadgaar hook task-completedonTaskCompleted; an install over 0.1.4 or older removes the eleven inert hook entries those versions wrote, an install over 0.1.31 or older removes theyaadgaar hook pre-tool-guardentry, and install never touches another tool's hooks), the MCP entry in~/.claude.jsonunder the keyyaadgaar(tools appear asmcp__yaadgaar__*; an existing entry under that key that this client did not write makes install refuse, and the separateyadgarkey is never touched), and the rules file~/.claude/yadgar-rules.md, referenced from~/.claude/CLAUDE.mdby the line@<home>/.claude/yadgar-rules.md(absolute path, matched exactly). If that line is already anywhere inCLAUDE.md, install leaves the file alone; otherwise it adds the line at the top. IfCLAUDE.mdis a symlink (for example nix-managed), install never edits it: when the line is missing it prints the exact line to add to whatever generates the file, and finishes the rest of the install. Ifclaudeis not on PATH it says so and registers anyway.yaadgaar installName the project. Each call carries a project key, read once when the agent starts
yaadgaar serve, from the directory the session starts in: the first.yadgar/project-idfile found walking up from it, otherwise the gitoriginremote reduced toowner/repo. With neither, no key is sent and the gateway refuses calls that need one; the client adds a reason to that refusal that names.yadgar/project-id. A deployment whose gateway runs project validation in enforcing mode also refuses a key that is not registered or that does not resolve, and this client has no command that registers one. The client does not check the key itself. It sends the key it found, and the gateway decides. The refusal reaches the agent as a tool error that holds the gateway's own message:yadgar gateway answered <status>: <message>. The client adds nothing to that message when it did send a key. Ask the deployment's admin to register the key, or write the registered key into.yadgar/project-id.mkdir -p .yadgar && printf '%s\n' 'owner/repo' > .yadgar/project-idA key can also be refused with HTTP 403 and the reason
PROJECT_NOT_MEMBER, when the gateway runs project validation inenforcingmode. Incountingmode the call is counted and served, and the agent sees no 403. The account may not reach that project. Three shapes give this answer: alocal/…project that another account owns (refused even when it is not registered), a project that has another account'slocal/…project beneath it, and an organisation project that none of the account's teams holds a grant on (ADR-0962). The organisation case needs both settings: project validationenforcingANDorgGrants: enforcing. A read and a write get the same answer. WithorgGrants: enforcingthe message is the same for every shape, so it does not say which one applied. The agent seesyadgar gateway rejected the credential (403 Forbidden): <message>. That text does not contain thePROJECT_NOT_MEMBERtoken. The token is only inerror.data.reasonand in the gateway's refusal metric, because the client drops the reason. The credential is valid, and logging in again as the same account does not change the answer. For a personal project, use your own prefix (see Finding your private project prefix). For an organisation project, ask an administrator for a team grant. UnderorgGrants: enforcing, a team that holds only a read grant getsPROJECT_READ_ONLY(also 403) on a write.Verify, now and on a schedule. It exits non-zero when the installed environment has drifted from what
installleft.yaadgaar verify
The config lives in ~/.config/yaadgaar/ (the OS config directory on macOS
and Windows; YAADGAAR_CONFIG_DIR overrides it). On Linux and macOS,
config.json and its directory are owner-only; on Windows the client sets no
permissions and relies on the profile directory's own.
Up to 0.1.4 the config lived in ~/.config/yadgar/, which is also where the
yadgar memory engine keeps its config.yaml and secrets.env. This client no
longer reads, writes or moves anything there. If it finds its old
config.json or tools-cache.json there and no new config, install,
verify and serve print a hint: run yaadgaar login (or yaadgaar enrol)
again. A new login mints a new install id.
The PreToolUse agent guard (yaadgaar hook pre-tool-guard, yaadgaar guard)
was removed after 0.1.31. It is harness control, not memory, so it is out of
yadgar's scope. Until you run yaadgaar install again, a leftover entry does
nothing, and yaadgaar verify names it.
Packaging
Rust, shipped to PyPI as wheels via maturin. The language follows the rest of the
system; the channel follows the people already using it, and a pip-installable command
is not a thing a rewrite gets to take away. A plain GitHub-release binary
was rejected for exactly that reason.
The wheel carries the compiled binary and no Python at all — bindings = "bin"
in pyproject.toml — so import yaadgaar is not a thing and is not meant to be.
A wheel is per-platform, so there are six: Linux, macOS and Windows, each on
x86_64 and aarch64. Every one is built on a runner of its own architecture rather
than cross-compiled, and release.yaml refuses to publish unless all six are
present — shipping the four that built would leave two platforms silently
installing an older version.
Development
cargo build
cargo test
pre-commit run --all-filesThere is no Containerfile and no chart here: this repository is not a service,
and what it ships is a wheel. CI is the shared workflow in
yadgarhq/actions, which detects that and
skips the image, chart and proto stages (D62).
To build a wheel locally:
maturin build --release # → target/wheels/, for THIS machine onlyThe other five targets are built by .github/workflows/release.yaml, which is
this repository's one piece of local CI. The reason it is not in
yadgarhq/actions with everything else is
written at the top of that file.
The Claude Code task-format canary
The client will mirror yaadgaar tasks into Claude Code's own task list
(ADR-1000, plans/harness-task-sync.md in yadgarhq/docs). Claude Code does
not document that list's file format, so the format is recorded in
tests/fixtures/claude-code/task-shape-v1.json, and nothing writes to a list
that does not match it (yaadgaar harness check-shape <dir> runs the check).
make canary checks the recording against the real thing. It runs claude -p
on a throwaway list (CLAUDE_CODE_TASK_LIST_ID=yaadgaar-canary-<random>) with
capture hooks from a temporary --settings file, then checks the files it
wrote, the TaskUpdate and TaskCompleted hook payloads, and the order those
hooks fire in. It needs claude logged in and costs a few cents, so it runs on
the operator's machine and never in CI. Run it after every Claude Code upgrade.
A red canary means the format moved: update the fixture and the check together,
in one reviewed change. make canary-record also rewrites
tests/fixtures/claude-code/hook-payloads.jsonl from the run.
Which tasks are mirrored is harness.scope, set in harness.json beside
config.json ({"scope": "active"}): all-open (every task not done or
dropped, the default per ADR-1000), active (in_progress and blocked), or
none. The file may also hold completion_ttl_secs (below). Any other key, a
typo included, is refused and named in the banner, never read as the default. The
task tools count as present when CLAUDE_CODE_ENABLE_TODO_TOOLS is exactly 1
(the documented value), or the model has them by default.
In a Claude Code session whose list passes the probe, session-start writes
each task in scope into the session's task list, keyed by its URN (never by its
yaadgaar number), updates the ones yaadgaar changed, and deletes the ones that
went done, dropped or out of scope. It never touches a task file it did not
write. What it owns is recorded outside the Claude Code tree, in
<state>/harness/claude-code/<list id>.json, where <state> is
$YAADGAAR_STATE_DIR, else $XDG_STATE_HOME/yaadgaar, else
~/.local/state/yaadgaar (the directory is 0700, the files 0600). The same
<state> holds the per-project registry record the memory engine reads
(<state>/registry/<key>.json), so one variable moves both. The banner
gains one line with the counts, or the reason the list was not synced.
An agent's edit to a mirrored task is written back to yaadgaar before it lands:
yaadgaar hook task-update (PreToolUse, matcher TaskUpdate) sends a subject
edit as edit_task and a pending/in_progress change as transition_task,
with the version last seen, and refuses (exit 2) what yaadgaar does not take. A
completion is closed by yaadgaar hook task-completed (TaskCompleted), and
only when the same session's same prompt marked it through task-update within
completion_ttl_secs (default 600). A state file or harness.json that cannot
be read refuses an edit to a mirrored task, with the reason, rather than let it
bypass write-back.
harness.scope = none and the kill switch YAADGAAR_HARNESS_SYNC=off both stop
the sync. Neither leaves a stale mirror behind: the pass that sees either one
only DELETES the files it owns, and writes nothing new. A list it never wrote
to, or a session whose list fails the probe, is not touched at all.
The recorded format (Claude Code 2.1.296):
<CLAUDE_CONFIG_DIR or ~/.claude>/tasks/<CLAUDE_CODE_TASK_LIST_ID or session id>/holds one<N>.jsonper task, a 0-byte.lock, and.highwatermark, a decimal integer written when a task is deleted. Ids are never reused.A task file holds
id(the file name, as a string),subject,description,status(pending,in_progressorcompleted),blocksandblockedBy(arrays of ids), and optionallyactiveForm,ownerandmetadata(an object, stored as given).TaskUpdatewithstatus: "deleted"removes the file.For one completing
TaskUpdate, the hooks fire asPreToolUse, thenTaskCompleted, thenPostToolUse.PreToolUseandTaskCompletedshare the sameprompt_id;TaskCompletedcarries notool_use_id.
This server cannot be deployed
Related MCP Connectors
MCP federation gateway: route, govern and observe agent tools across clouds and MCP servers.
1Toolstile is a managed MCP gateway that connects remote MCP servers behind one endpoint, with per-agent permissions, shared accounts and activity logs. Connect at https://mcp.toolstile.com/mcp over Streamable HTTP with OAuth; sign in at https://toolstile.com, select a workspace and connect your upstream servers. Available tools, resources and prompts depend on that workspace and the agent's granted access.
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
MCP gateway and cloud hosting for MCP servers, agents and apps like RAG, managed from your agent.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA gateway server that enables agentic hosts to access multiple MCP servers through a single namespaced connection or proxy a specific server from MCP-Hive. It provides built-in discovery tools to list available servers, tools, and resources for seamless integration.125 npmApache 2.0
- AlicenseNot gradedqualityCmaintenanceA gateway that aggregates multiple MCP servers into a single endpoint, namespacing their tools and forwarding calls, so an agent connects to one MCP to access the entire stack.MIT
- AlicenseNot gradedqualityDmaintenanceActs as an MCP gateway aggregating multiple child MCP servers into a single namespaced interface, with an optional memory layer that caches tool results to reduce redundant calls.4 npmMIT
- AlicenseAqualityDmaintenanceEnables models to discover, inspect, and call tools from thousands of MCP servers on the fly through a single gateway, without pre-configuring each server.554 npm3MIT