Repository navigation
feat(cli): support a single-clone global install shared by several CLIs #859
Description
Problem
The framework installs per project, through a marketplace and the host's plugin
system. There is no supported path for a machine-wide install shared by several
CLIs from a single clone — which is what a solo developer running four agentic
CLIs side by side actually needs.
Working around it surfaced three concrete gaps, all on v5.10.0 with
@ai-driven-dev/cli@5.3.0:
1. translate --as flat emits hook files but never their declaration.
plugins/aidd-telemetry/hooks/hooks.json declares the three events the journal
needs (SessionStart, Stop, PostToolUse, lines 8/18/28, keyed on
${CLAUDE_PLUGIN_ROOT}). Neither --to claude --as flat nor --to codex --as flat
reproduces it: both write hooks/aidd-telemetry/journal.cjs and its lib/, and no
hooks.json anywhere in the output tree. A hook that is placed but never declared
does not fail — it simply never runs, silently. Outside the plugin runtime that
substitutes ${CLAUDE_PLUGIN_ROOT}, the declaration has to be hand-written into
each host's own config format.
2. Telemetry silently records nothing on two shipping CLIs.
DECLARED_HOSTS (plugins/aidd-telemetry/hooks/lib/host.cjs:25) covers
claude-code, codex, copilot, cursor, opencode. Kimi Code and Antigravity
(agy) are absent, so detectHost returns null and
handleUnrecognisedPayload (lib/record.cjs:255) writes a single
_unrecognised.jsonl marker line instead of a run file. Both CLIs load and run
AIDD skills perfectly well — only the measurement layer cannot read them.
3. framework build was removed without a deprecation path.
CLI 5.x split it: lifecycle stayed on framework, conversion moved to
translate, and the options were renamed (--source → positional, --target →
--to, --flat → --as flat). An install script written against 5.x-minus-one
dies on error: unknown command 'build' with no pointer to the replacement.
Scope
- Document (or support) the single-clone, multi-CLI global install as a first-class
layout, distinct from the per-project marketplace install. - Make
translate --as flatemit each target's hook declaration alongside the hook
files, with absolute paths substituted for${CLAUDE_PLUGIN_ROOT}. - Add Kimi Code and Antigravity to
DECLARED_HOSTSand todetectHost, with the
payload shape each one sends. - Publish the CLI availability matrix below in the docs, so an integrator knows what
is portable before writing a deployment script.
Acceptance criteria
-
aidd translate <src> --to claude --as flat --out <dir>writes a hook
declaration for every hook it installs, with no${CLAUDE_PLUGIN_ROOT}left
unresolved. - Same for
--to codex --as flat. - A session run in Kimi Code produces a run file under
aidd_docs/runs/, not a
_unrecognised.jsonlmarker, when the project has opted in. - Same for Antigravity.
-
aidd framework buildexits with a message namingtranslateand the renamed
options, rather thanunknown command. - The docs carry a per-surface, per-CLI support table.
Prior art in this repo
plugins/aidd-telemetry/hooks/hooks.json— the declaration that exists upstream
and is dropped by the flat layout.plugins/aidd-telemetry/hooks/lib/host.cjs:25—DECLARED_HOSTS.plugins/aidd-telemetry/hooks/lib/record.cjs:250-269— the unrecognised-payload
path, which is exactly why the gap is silent rather than loud.plugins/aidd-telemetry/hooks/lib/repo.cjs:141—telemetryEnabled, the strict
=== trueopt-in gate. Worth noting: the global install stays inert by default
because of this, which is what makes a machine-wide hook acceptable at all.plugins/aidd-context/hooks/update_memory.js— host-agnostic by construction: it
rewrites the open project'sCLAUDE.md/AGENTS.mdand never reads who called
it. This is why it works on all four CLIs while the journal works on two.
Out of scope
- Agent deployment to Antigravity.
agyexposes--agentand anagents
subcommand, butagy agentsreturns nothing and file-based profiles dropped in
~/.gemini/config/agents,~/.gemini/agentsand~/.gemini/config/subagentsare
ignored. Its subagents look internal rather than file-declared — nothing to
deploy until that changes. - Turning measurement on. That stays per project via
.aidd/config.json, as today.
How the global install works
A ~250-line shell script, idempotent, re-runnable. The design decisions are the part
worth discussing — several of them are constraints a framework install --global
would hit too.
One clone is the source of truth, and it is not vendored. The framework lives at
~/dev/aidd-framework; my repo versions how to deploy it, never its content. A
--pull flag fast-forwards the clone, and the version reported is the latest v*
tag — not git describe, which picks up the per-plugin tags (aidd-context-v2.7.0)
first, and not the root package.json, still at 0.0.0.
Two builds, four destinations. translate --to claude --as flat provides the
skills — identical for all four CLIs — plus the Markdown agents and the hooks.
--to codex --as flat is built only to harvest the TOML agents. Everything is
staged in a temp directory first, so a failed build never touches a live config;
when framework build disappeared in CLI 5.x, set -e stopped the run before a
single file moved.
Four skill roots, one format. ~/.claude/skills, ~/.codex/skills,
~/.kimi-code/skills, ~/.gemini/config/skills. All four load the same
<name>/SKILL.md bundle with no translation — this is the single most portable
thing in the framework.
Every write is prefix-scoped to aidd-. Nothing outside that prefix is written
or removed, so a cleanup pass can never eat a hand-written skill living in the same
directory. A bundle that disappears upstream is removed locally: without that, a
renamed skill lingers as a duplicate and keeps being offered to the model. Bundle
directories are replaced wholesale rather than merged — the telemetry hook loads
hooks/lib/, and a module dropped upstream must not survive locally as dead code
the entry point still requires.
The script never writes a CLI's configuration file. ~/.claude/settings.json,
~/.codex/hooks.json, ~/.gemini/settings.json and ~/.kimi-code/config.toml
already carry hooks that are not mine. Instead it verifies two things per file — a
declaration exists, and the path inside it resolves to a real file — and prints the
block to paste when either fails. The second check is the one that matters: a
declaration left pointing at an old path fails silently forever.
One wrinkle worth flagging for anyone automating this: JSON allows \/ for a
forward slash, and one of these files is written in that dialect. Searching for the
hook path verbatim reports a perfectly valid declaration as missing.
A separate consistency checker, at two levels. Disk-only is instant and free: it
fingerprints the aidd- bundles across the four roots and compares them, so a
partial deployment shows up as a fingerprint mismatch. --live adds one real model
call per CLI — the probe opens a skill bundle and asks for the filenames in its
actions/ directory, which fails when the bundle is present but stripped of its
companion files. The two levels do not prove the same thing, and the first does not
replace the second: a CLI that has not been restarted after an update shows a
perfect disk and still sees nothing.
Where the hooks live. ~/.aidd/hooks/, a neutral root — not under ~/.claude/,
which would put Codex's configuration inside another tool's directory. The memory
hook goes to all four CLIs because it never reads who called it; the telemetry
journal goes to two, for the host-detection reason above.
What I measured
One clone of the framework, built twice with translate (--to claude for skills,
Markdown agents and hooks; --to codex for TOML agents), then deployed to four CLIs.
50 skills, 2 agents. Each row below was checked by running the CLI, not by reading
its docs — a probe opens a skill bundle and lists its actions/ directory, so it
fails when the bundle is present but stripped of its companion files.
| Surface | Claude Code | Codex | Kimi Code | Antigravity |
|---|---|---|---|---|
Skills (<name>/SKILL.md) |
yes | yes | yes | yes |
| Agents | yes (.md) |
yes (.toml) |
yes (.md, Claude format verbatim) |
no target format |
Memory hook (update_memory.js) |
yes | yes | yes | yes |
Telemetry hook (journal.cjs) |
yes | yes | no — host unknown | no — host unknown |
The skill bundle format is the portable one: all four load the same
<name>/SKILL.md tree with no translation. Kimi reads the Claude agent Markdown
as-is. Hook declaration is where the four diverge — Claude Code and Codex share a
JSON shape, Antigravity uses a similar JSON under ~/.gemini/settings.json, and
Kimi uses flat TOML [[hooks]] entries.
Happy to open this as separate scoped issues if you would rather validate them one
at a time, and to take the telemetry host-detection one myself.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Projects
- StatusShow more project fieldsDone
Activity
blafourcade commentedon Sep 14, 2026
This issue mixes work that is already complete with two new host integrations.
Already resolved on current
next:translate --as flatmerges hook declarations into.claude/settings.jsonand.codex/hooks.json; feat(cli): one shared framework source per machine, user scope and session telemetry #795 added integration coverage.framework buildwas intentionally replaced bytranslate. Reintroducing the old command would restore a retired CLI surface.The remaining question is Kimi Code and Antigravity compatibility.
Keep this issue as a Spike to verify, for each host:
aidd-telemetry;Kimi Code uses user skills under
~/.kimi-code/skills/and hook declarations in~/.kimi-code/config.toml. Antigravity’s global integration is a plugin under~/.gemini/config/plugins/, which can carry skills, rules, MCP and hooks. They therefore cannot share the existing Claude/Codex configuration logic unchanged.Out of scope: a framework installer rewriting global configuration for all tools, a
framework buildalias, and further Claude/Codex flat-hook work.The spike should end with a small compatibility table and separately scoped implementation tasks only where the evidence proves support.
dkp-consult commentedon Sep 17, 2026
TL;DR — both hosts are supportable. Kimi Code needs a tool profile plus a journal host;
Antigravity needs a journal host and turns out to have three install targets, one of them
machine-wide. Patches exist for both. Two unrelated defects found on the way, kept out of this
thread.
Spike done. Everything below was measured on a local checkout of today's
next, not read in adoc —
agy1.2.5, Kimi Code 0.43.1, CLI 5.3.0 built from source. Most claims carry the commandthat reproduces them, so you can check the ones that matter to you rather than take my word.
Version note that matters for the decision:
agyupdated itself three times during this spike(1.2.1 → 1.2.5) and Kimi moved 0.40.1 → 0.43.1. Anything pinned to those binaries is perishable
by construction.
Compatibility table
agy)<name>/SKILL.md)~/.kimi-code/skills/and.agents/skills/, user + project~/.gemini/config/skills/and<project>/.agents/skills/.md.toml.mdverbatim —.kimi-code/agents/,.agents/agents/, both scopes<name>/agent.md, a directory like a skill —<project>/.agents/agents/and~/.gemini/config/agents/. Not a flat.md.CLAUDE.mdAGENTS.mdAGENTS.mdrules/*.mdin any customization root,GEMINI.md,AGENTS.mdsettings.jsonhooks.json[[hooks]]inconfig.tomlhooks.json, and only that<project>/.agents/hooks.json, gated by folder trustconfig.toml(its only scope)~/.gemini/config/hooks.json, no plugin packagingjournal.cjs)Kimi's roots are named in its own bundle (
PROJECT_GENERIC = [".agents/skills"],USER_GENERIC = [".agents/agents"], …) and its importer says it out loud: "Do not migrate.agentscontent. Kimi Code already supports.agentsskills and AGENTS files by default."Antigravity's are in its embedded help: "Workspace Customizations Root … Path:
.agents", and"Within any of the customization roots above, you can define: Skills, Rules, Plugins, MCP
Servers (
mcp_config.json) and Hooks (hooks.json)."The symptom, reproduced on today's
nextIn a fresh opted-in repo, unpatched journal: a Kimi
SessionStartand an AntigravitySessionStartboth produceaidd_docs/runs/_unrecognised.jsonland nothing else.detectHostmisses every payload from both. Run from a
cwdoutside any repository, the journal writesnothing at all — not even the marker — and exits 0.
Antigravity: the gate is folder trust
My first comment said
agyhad no project hook scope and no way around an emptyworkspacePaths. Both wrong: I had tested four paths under.gemini/and.agy/, and thecustomization root is
.agents/. What decides whether a project hook runs is whether thefolder is trusted, not whether the session is interactive — and
agywrites it in its own logat every start (
~/.gemini/antigravity-cli/log/cli-*.log):Eleven milliseconds apart: that is the answer to "Yes, I trust this folder". Its own changelog
confirms the mechanism — "Fixed workspace-local hooks defined in
<workspace>/.agents/hooks.jsonnot loading after trusting a folder by reloading hooks whenever workspaces change."
--add-dir <repo>opens the same door headless with no trust granted at all. (named hookscounts top-level groups, not events.)
Three install targets, not two.
<project>/.agents/hooks.json, a global plugin, and~/.gemini/config/hooks.json— the global customization root, which I had flagged last round asuntested by anyone, mine included. It is tested now: a journal wired only there, with the
project hook disabled, writes a correct run file inside the repository
(
tool:"antigravity",session_start→step_starton a skill →turn_end), because the hostresolves the repo from
workspacePaths[0]rather than from its owncwd(~/.gemini/config).That is a native machine-wide target needing no plugin packaging. I removed the file afterwards;
the log going back to
1 named hooks from 1 fileis the closing witness.Three findings that outlive my conclusions
1.
SessionStartis a hook event, and it is undocumented. The embedded guide lists fiveevents; the embedded proto carries six (
exa.hooks_pb.SessionStartHookArgs). Declared in ahooks.jsonit fires with the full payload andagy plugin validateaccepts it. So the journalmaps 1:1 —
SessionStart→session-start,Stop→turn-end,PostToolUse→tool-used—with no need to repurpose
PreInvocation, which 1.2.5 reports as "deprecated and has noeffect" anyway.
2.
agysays which surface it loaded, for free. Onegrepincli-*.log, oragy -p "/hooks", which answers in print mode without starting a turn or spending quota. Two ofmy passes built probe plugins to obtain a fact the binary writes down on its own.
3. A defect in
aidd-telemetryitself, fixed in task C.handleFileWrittenresolved therepository from
payload.cwdinstead of the host'sreadCwd. Any host whose payload carries nocwd— Antigravity names onlyworkspacePaths— had every stated write silently degraded tothe observed pass. Same payload, same repo, hook
cwdoutside it: before the fix the run fileholds
session_startalone, after it holdssession_start+file_written(
source: "tool-stated"). No declared host changes behaviour — the four withwrittenPath: null(Codex, Copilot, Cursor, OpenCode) never reach that line, and Claude Code and Kimi declare
readCwd: (payload) => payload.cwd, so the value is identical for them.Kimi Code
I have a working patch for this, if it is useful to you. On a clean checkout of today's
nextit applies as-is and takes the journal suite from 197 to 201 and the thirteen telemetry
suites from 310 to 314, with five fixtures and four tests of its own. It is yours to take,
adapt or ignore — I mention it only so the work is not done twice.
The payload contract is confirmed on 0.43.1, from a fresh capture, not from the fixtures. I
re-authenticated, declared a probe hook, ran two real sessions, and removed the declaration
afterwards (the file is byte-identical to its backup). What came back matches the fixtures field
for field:
client_type: "kimi_code_cli",session_idandcwdin Claude's spelling, notranscript_path, a write astool_name: "Write"withtool_input.pathrelative tocwd,and a skill call as
tool_name: "Skill"withtool_input.skill. Replayed through the patchedjournal in a fresh opted-in repository, those live payloads produce
session_start→file_written(source: "tool-stated"— the relative-path resolution holds) →turn_end, andstep_startnaming the skill, all with"tool":"kimi-code".One packaging detail found on the way, relevant to D: Kimi requires
namein a directoryskill's frontmatter and silently ignores the skill without it, where
agyfalls back to thedirectory name. AIDD's own skills all carry
name, so nothing is broken today — but a shared.agents/tree has to satisfy the stricter host.One consequence to decide up front: Kimi has no project hook scope (
kimi doctorfrom inside arepo lists only the global
config.toml; its importer refuses to migrate hooks), so thedeclaration must be global, merged idempotently into a
config.tomlthat is not ours — on thismachine it already carries fifteen
[[hooks]]entries from another tool. A global hook staysinert until a repo opts in (
telemetry.enabled === true, strict, plus anAIDD_TELEMETRY=0personal refusal). A cost to assume, not a blocker.
What I withdraw
"No project-level hook scope in
agy" — wrong namespace. "workspacePathsis empty on everyevent" — true only of
-pwithout--add-dir. "Nobody has tested~/.gemini/config/hooks.json"— no longer true. My
GEMINI_API_KEYnote — the variable comes from this machine's shell, Inever ran the control. "No target format for agents" — a Claude
.mdpassesplugin validate.And you were right about rules: no
rulescomponent in a plugin, butrules/*.md,GEMINI.mdand
AGENTS.mdare read from any customization root. Point 3 (framework build): withdrawn.Point 1 is verified fixed on today's
next:--to claude --as flatand--to codex --as flatboth emit a resolved hook declaration, with zero unresolved
${CLAUDE_PLUGIN_ROOT}across 467and 468 written files.
Tasks
~/.kimi-code/, skills, Claude-format agents, a TOML[[hooks]]writer,telemetryJournalHost: "kimi-code". Prerequisite to B.kimi-codejournal host.antigravityjournal host, as a delta of B:detectHostonconversationId+transcriptPath, repo fromworkspacePaths[0], 1:1 event mapping,write_to_file/TargetFileextractor, skill step read off theSKILL.mdview, plus thehandleFileWrittenfix above. Documents the trust step..agents/as a portable target: Kimi and Antigravity both read it, andtranslate --to codex --as flatalready writes that tree today — 51 skills, and agents would be the two inaidd-dev. Two hosts, no new profile. Independent of A.On agents specifically. Two files, but a silent failure mode, which is the only reason it
gets a paragraph. Both hosts read
<project>/.agents/agents/, and not in the same shapes: Kimiaccepts a flat
<name>.mdand<name>/agent.md, Antigravity accepts only<name>/agent.md— a directory, like a skill — at<project>/.agents/agents/or~/.gemini/config/agents/. The directory shape is therefore the intersection, and it ismeasured on both: written once,
kimi --agent <name>andagy --add-dir <repo> --agent <name>each execute the agent's own prompt, headless, with no folder trust needed.
The flat shape fails quietly in three different ways, which is what makes it worth a warning.
agynever surfaces it.agy --agent <name>then falls back to the default agent without aword, and an invented name behaves identically, so the flag confirms nothing either way — where
Kimi refuses an unknown name and lists the profiles it found. And
agy plugin validatereportsagents : 1 processedfor a flat.mdthe loader will never load, so a green validation is notevidence. Today
translate --to codex --as flatwrites.agents/skills/only, with agents goingto
.codex/agents/*.toml, so D would be adding that tree — the shape it should take is settled.Happy to split, drop or reorder any of this — you know the roadmap, I only know what I measured.
If B and C are useful as PRs I can open them, one at a time rather than bundled; if you would
rather implement them yourself, the findings above are all you need and the patches are just a
shortcut.
Two things I kept out of this thread
Both measured, neither about Kimi/Antigravity telemetry:
~/.gemini/settings.jsonin the Gemini-CLI vocabulary (BeforeAgent,AfterAgent,BeforeTool,AfterTool), four strings with zero occurrences in the 1.2.5 binary, whosehook manager loads
hooks.jsonfiles only. A hook in a file the tool never reads isindistinguishable from a hook that ran and did nothing.
aidd setup --scope user --plugins …throws an uncaughtUserScopePluginModeErrorwith araw stack trace, where
plugin install --scope userrefuses the same thing cleanly("Tool 'claude' does not support scope 'user'"). The message is good, it is just thrown
instead of rendered. A framework installer rewriting global configuration is out of scope by
your own ruling — this is error hardening plus documenting the real
installScope: "user"matrix, since the first half of a user-scope install already works.
Want me to open these as two separate issues? Both are written up with their reproduction
commands — say the word and I file them, or I fold them in here if you would rather keep one
thread.
blafourcade commentedon Sep 17, 2026
@dkp-consult Yes to both. Open the PRs, one at a time as you proposed, and open the two unrelated defects as their own issues.
Thank you for the withdrawals as much as the findings — a spike that names what it got wrong last round is worth more than one that only reports wins.
Order, and one change to it.
Split the
handleFileWrittenfix out of task C and send it first, on its own.It is a defect in shipped code, not part of a new host:
handleFileWrittenresolving the repository frompayload.cwdinstead of the host'sreadCwddegrades every stated write to an observed one for any host whose payload carries nocwd. That it currently harms no declared host is what makes it worth landing alone — it reviews in minutes, it needs no roadmap decision, and burying it inside an Antigravity PR means it waits on a host we have not scheduled. Your before/after (session_startalone, versussession_start+file_writtenwithsource: "tool-stated") is the regression test.Then:
antigravityjournal host) next. It is autonomous, you have proven it over the wire in both scopes, and it does not wait on A.[[hooks]]writer and a global-only hook scope are both new shapes for the CLI registry, and merging idempotently into aconfig.tomlwe do not own is a commitment, not a detail. Open it as an issue with the surfaces you measured and I will rule on it there, before you spend more on B..agents/as a portable target) independently, whenever. Two hosts reading one tree we already write is the cheapest win in your table.On the agents shape, your directory-intersection finding settles it and should be written down where it will be read, not only in this thread:
<name>/agent.mdis the only shape both hosts load, and the three silent failures of the flat shape —agynever surfacing it,--agent <name>falling back without a word,plugin validatereportingagents : 1 processedfor a file the loader ignores — belong in D's issue body. A green validation that is not evidence is exactly the kind of claim this project has been burned by.Two constraints to carry into the PRs, neither a blocker:
agymoving 1.2.1 → 1.2.5 mid-spike is the argument. Where a capability depends on an undocumented event (SessionStartin the proto but not the guide), the code detects rather than assumes, and the reference cell carries the date you read it — that is the rule fix(aidd-context): source-backed tool references, with dates and unverified cells #618 exists to establish.Finding 2 — that
agywrites which surface it loaded intocli-*.log, and answers/hooksin print mode without spending quota — is the most reusable thing in the report. Put it in C's issue or reference docs so the next person does not build a probe plugin for a fact the binary volunteers.One question back, so B and C do not end up blocked on review: does your
.agents/work assume the machine-wide~/.gemini/config/hooks.jsontarget, or is it independent of it? You proved that target works and resolves the repo fromworkspacePaths[0], but a machine-wide install rewriting global configuration is the thing this issue originally asked for and that I have not ruled on. If D stands without it, keep them separate.Generated by Claude Code
fix(telemetry): resolve the repository from the host, not from payloa…
fix(telemetry): resolve the repository from the host, not from payloa…
blafourcade commentedon Sep 24, 2026
Spike conclusion
The compatibility question is resolved. Kimi Code and Antigravity are supportable, but a single generic global installer is not the owning abstraction: portable artifacts can be shared, while hook declarations and configuration ownership remain host-specific.
Follow-up ownership:
.agents/agents/<name>/agent.md→ feat(cli): emit portable Markdown agents under .agents/agents #916setup --scope user --pluginsvalidation error → fix(cli): setup --scope user --plugins throws an uncaught UserScopePluginModeError instead of rendering a validation error #891The original flat-hook and retired
framework buildconcerns were withdrawn or already fixed onnext. The spike's evidence and remaining delivery are now durably owned, so #859 can close as completed without waiting for the successor Features and Bugs to ship.