Probe plugin for the R3 function-hook question: answers tool.list with one named tool removed and writes one marker file per call. Used by…

<!-- markdownlint-disable MD033 MD041 -->
<img src="docs/banner.png" alt="OrchestKit - Stop explaining your stack. Start shipping." width="100%" />
<!--ork:skills-->110<!--/ork--> skills · <!--ork:agents-->36<!--/ork--> agents · <!--ork:hooks-->169<!--/ork--> hooks
<a href="https://orchestkit.yonyon.ai/"><strong>Explore the Docs →</strong></a> · <a href="https://yonyon.ai/go/orchestkit?utm_campaign=readme"><strong>OrchestKit Community →</strong></a><br> <sub>Skill browser, demo gallery, setup wizard</sub>
Pick the host you actually use. Claude Code is the full plugin (skills + agents + hooks). Cursor gets the same ork plugin minus Claude hook scripts. skills.sh is skills only — start with the 12 below, not the whole catalog.
Measured 2026-09-08 on pi 0.85, Codex CLI and cursor-agent. Details, commands and the lane model: OrchestKit on pi, Codex and Cursor. Antigravity measured 2026-09-18 on agy 1.2.6; full evidence in docs/audits/agy-host-support-2026-09-18.md.
| Surface | Claude Code | Cursor | Codex | pi | Devin | Antigravity |
|---|---|---|---|---|---|---|
| Skills (SKILL.md) | all | all, via the ork plugin | 6 (ork-codex pack) | all via pi install, 78 auto-listed | 76 of 107, GH-4146 | all via workspace .agents/skills (skills.sh); ork:<name> via agy plugin install |
| Agents | all | all | 4 role templates | none | not reported by info | 36 via agy plugin install (validated); none via .agents/skills |
| Hooks | all | none | none | none | none | none of ork's; agy hooks.json is a different schema |
| Rules | repo convention | 14, plugin rules key | AGENTS.md | none | AGENTS.md, always on | AGENTS.md, per-directory |
| Commands | /ork:<skill> | 36 wrappers | $ork-<skill> | /skill:<name> | /ork:<skill> | /<skill-name>; ork:<skill> via plugin install |
| MCP config | .mcp.json | .cursor/mcp.json | plugin mcp.json | .pi/mcp.json | mcp.json / .mcp.json | ~/.gemini/config/mcp_config.json or plugin mcp_config.json; repo .mcp.json not read |
| Status | shipped | shipped | shipped | shipped | skills only, GH-4146 | skills only, measured agy 1.2.6 |
/plugin marketplace add yonatangross/orchestkit
/plugin install ork
Then /ork:setup. The wizard scans the repo, recommends skills, and writes MCP config.
CLI equivalent: claude plugin marketplace add yonatangross/orchestkit && claude plugin install ork@orchestkit.
Settings → Plugins / marketplaces → add yonatangross/orchestkit → enable ork → open a new chat. Same plugin Claude Code installs, not a five-skill fork. See Install → Cursor.
Starter 12 from measured installs plus the ork skills we retain (full why table under Install):
npx skills add yonatangross/orchestkit -s devops-deployment -s responsive-patterns -s architecture-decision-record -s ui-components -s rag-retrieval -s agent-orchestration -s brainstorm -s expect -s auto -s review-pr -s fix-issue -s commit
Full catalog: npx skills add yonatangross/orchestkit (hundreds of SKILL.md files; do not treat that as unique users). Live install totals stay on the skills.sh badge above.
Every Claude Code session starts from zero. You explain your stack, patterns, preferences—again and again.
OrchestKit gives Claude persistent knowledge of production patterns that work automatically:
| Without | With OrchestKit |
|---|---|
| "Use FastAPI with async SQLAlchemy 2.0..." | "Create an API endpoint" → Done right |
| "Remember cursor pagination, not offset..." | Agents know your patterns |
| "Don't commit to main branch..." | Hooks block bad commits |
| "Run tests before committing..." | /ork:commit runs tests for you |
One unified plugin, everything included.
| Component | Details |
|---|---|
| <!--ork:skills-->110<!--/ork--> Skills | RAG patterns, FastAPI, React 19, testing, security, database design, ML integration — loaded on-demand, zero overhead |
| <!--ork:agents-->36<!--/ork--> Agents | Specialized personas (backend-architect, frontend-dev, security-auditor) — route tasks to the right expert |
| <!--ork:hooks-->169<!--/ork--> Hooks | Pre-commit checks, git protection, quality gates, browser safety — ship with confidence |
All available in a single /plugin install ork. Skills load on-demand. Hooks work automatically.
Browse everything in the Docs →
/ork:auto # Front door: describe a goal, it routes to the right skill
/ork:setup # Personalized onboarding wizard
/ork:implement # Full-stack implementation with parallel agents
/ork:expect # Diff-aware AI browser testing
/ork:review-pr # PR review with parallel agents
/ork:verify # Multi-agent validation
/ork:commit # Conventional commit with pre-checks
/ork:explore # Analyze unfamiliar codebase
/ork:remember # Save to persistent memory
/ork:doctor # Health check
/ork:setup detects your stack, recommends MCP servers, and writes the configuration for you.
| Server | Purpose | Required? |
|---|---|---|
| Context7 | Up-to-date library docs | Prerequisite (22 of 36 agents grant its tools) |
| Memory | Knowledge graph persistence | Recommended |
| Sequential Thinking | Structured reasoning for subagents | Recommended |
| Tavily | Web search and extraction | Optional |
Set "alwaysLoad": true on the first three in your .mcp.json. It skips the per-skill tool probe and shaves ~150ms off cold starts.
Context7 is a prerequisite, and ork does not ship it. 22 agents grant mcp__context7__* in their frontmatter, but .mcp.json is user-owned and project-scoped, so the grant refers to a server you add. Skip it and those agents answer from training data with no error raised. The recommended entry is the hosted HTTP server, which costs no local process:
"context7": {
"type": "http",
"url": "https://mcp.context7.com/mcp"
}
Free tier: 1,000 requests, public repos, no account. Context7 Pro ($10 per seat per month) raises that to 5,000 per seat and parses private repos; add "headers": { "Authorization": "Bearer ${CONTEXT7_API_KEY}" } and export the ctx7sk- key. Add the header only once the variable is exported: with it unset the unexpanded literal is sent as the token and every query fails, and it does not fall back to the anonymous free tier, so the keyless entry above is strictly better than a header with no key behind it. The legacy stdio transport (npx -y @upstash/context7-mcp@4.0.2) is the fallback when the hosted endpoint is unreachable, but it spawns one child process per Claude Code session, so the fan-out scales with how many sessions you keep open.
Skills install as files on your disk, but don't hand-edit the installed copy — it gets overwritten on update and silently diverges from the canonical playbook. The supported ways to extend (user-level skills, project skills, upstream PRs, or disabling a bundled skill) are in docs/extending-skills.md.
OrchestKit is a quality-gate plugin, so its hooks are the product rather than an add-on. This section states plainly what they see, where it goes, and how to turn each piece off.
Scope: broad and intentional. OrchestKit registers <!--ork:hooks-->169<!--/ork--> hooks across <!--ork:events-->32<!--/ork--> lifecycle events, including SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop. They are not gated to a particular framework or project type, because the gates they enforce (secret-write blocking, protected-file guards, git safety, file-size limits, agent status protocol) apply to any codebase. If you only want gates on some projects, enable the plugin per-project rather than globally.
Where data goes: a local file on your own disk.
| What | Destination | Notes |
|---|---|---|
| Lifecycle events (session end, PR merged, goal converged, chain phase) | ~/.local/state/orchestkit/events.jsonl | Written unconditionally, rotated at 10 MB. ORK_EVENTS_LOG redirects the path (used by the test suite) |
| Hook metrics: event name, tool name, payload size, duration | same local file | Size-capped metrics only |
| Prompt text and file contents | Never recorded | Hooks read them to make an allow/deny decision, then discard |
| Remote sync | Off | No endpoint is compiled in; see below |
There is deliberately no global kill switch for the local write, because the gates depend on that state (the git-safety and chain-staleness hooks read their own prior events). To stop it entirely, disable the plugin. Individual noisy hooks have their own opt-outs: ORK_DISABLE_DEBT_TRACKER, ORK_DISABLE_WORKTREE_VERIFIER, ORK_DISABLE_COORDINATION_METRICS, ORK_NO_NOTIFY, ORK_NO_STALE_SWEEP, and ORCHESTKIT_SKIP_SLOW_HOOKS among others.
Network access is opt-in and unset by default. There is no hardcoded remote host anywhere in the shipped hook bundles (grep -o 'https\?://' plugins/ork/hooks/dist/*.mjs returns nothing). An outbound call happens only if you configure a destination yourself, via one of:
ORCHESTKIT_HOOK_URL + ORCHESTKIT_HOOK_TOKEN, which enable the manual hooks/bin/telemetry-sync.mjs CLI. It POSTs your local JSONL to your own endpoint. No hook ever invokes it; you run it by hand.ORK_HQ_TELEMETRY_URL, which points the telemetry HTTP sink at your own collector.ORK_HQ_TELEMETRY_USE_HQ_API=1 together with HQ_API_URL, the same sink aimed at a self-hosted HQ API.ORK_SESSION_CATEGORY_PROVIDER=jev (or shadow) together with the TypeSafe key variable ORK_TYPESAFE_API_KEY, a second classifier for the session work category. The session-identity hook already asks a local claude -p --model haiku process for a title and a category; with both variables set it also asks TypeSafe's Jev model (api.typesafe.ai, model pinned to jev-1.13.0) one typed Choice over the same eight categories and the same criteria text. This is the one exception to the "no hardcoded host" note above, and it is dormant unless both variables are set. What leaves your machine: the git branch name and the first 600 characters of the session's first prompt, sent to a third-party processor under its own data policy. What changes in jev mode: when Jev answers at confidence 0.8 or above, its category decides the session color (held out on 150 sessions, that band is 93.5% correct); below 0.8, or on any error, haiku's category decides as before. The title and emoji always come from haiku. In shadow mode nothing you see changes; Jev is only logged beside haiku. To turn it on locally, in the shell that launches claude: export ORK_TYPESAFE_API_KEY="$(<your secret manager> ...)" and export ORK_SESSION_CATEGORY_PROVIDER=jev. Both outcomes are logged once per session in the hook log, as category jev: haiku=<cat> jev=<cat> agree=<bool> confidence=<n> decided_by=jev|haiku threshold=0.8 latency_ms=<n>, and as session-identity.shadow.json with the same fields (labels, decision, timing; never prompt text) next to the raw answer session-identity.jev.json in the session data directory. Cost of opting in: the first prompt of a session waits for the call to settle, 1.1 to 1.7 s measured from a fresh hook process (the eval's 320 ms was a warm connection), 3 s at most before it gives up.ORK_ROUTE_JEV=shadow (or steer) together with ORK_TYPESAFE_API_KEY, the Jev routing seam for /ork:auto (#4233). Off by default. When set, every build-shaped prompt (the same test the once-per-session executor reminder uses: an imperative build or fix verb, 40 characters or more, no explicit /plugin:skill) is sent to the same TypeSafe endpoint and model as one typed Choice over nineteen route classes plus four side judgments (needs a worktree, needs a browser, mutation risk, needs the operator). What leaves your machine: the first 1,500 characters of the prompt after a redactor has replaced secrets, emails, Israeli phone numbers, nine digit ids, op:// references and the names of directories under clients/ with [SECRET], [EMAIL], [PHONE] and [CLIENT], plus the repository basename. The serialized request is scanned again before it leaves; anything that survives redaction refuses the call. In shadow mode nothing you see changes; the verdict is logged. In steer mode, at confidence ORK_ROUTE_JEV_FLOOR (default 0.5) or above, the one-line executor reminder names the executor the class maps to (route: dev_fix -> /ork:fix-issue (conf 0.83)); the model still reads the /ork:auto table and says whether it agrees. Fallbacks are fail open: no key, timeout, non-2xx, malformed answer, below the floor, or the daily token budget (ORK_ROUTE_JEV_DAILY_TOKENS, default 2,000,000) spent all mean the existing path runs untouched; a 402 or 429 switches the seam off for 24 hours. One log line per prompt, route jev: intent=<class> conf=<n> top3=<a:p,b:p,c:p> worktree=<n> browser=<n> mutation=<n> operator=<n> floor=<n> decided_by=jev|table|off|budget|egress latency_ms=<n> input_tokens=<n> redacted=<n>, and one record per prompt in jev-route.jsonl next to session-identity.jev.json (labels, timings and a sha256 of the redacted text; never prompt text). Offline replay: node scripts/eval/route-check.mjs --jev and node scripts/eval/jev-route-score.mjs. The vendor's agent skill is a peer plugin installed from its own marketplace, never a copied directory: claude plugin marketplace add typesafe-ai/skills then claude plugin install typesafe@typesafe-ai; /ork:doctor reports the installed version against the marketplace and prints the update commands.The sink returns early when the URL or the token is missing, and telemetry-sync.mjs prints No ORCHESTKIT_HOOK_URL or TOKEN configured. Nothing to sync. then exits 0. There is no analytics ping, no crash reporter, and no feature-flag fetch.
What OrchestKit never reads. No OS keychain lookups, no ~/.aws/credentials, no SSH private keys, no browser cookie or login stores, no clipboard. The one place secret-shaped paths appear in the source is plugins/ork/hooks/dist/pretool.mjs, where id_rsa, .pem, .env, and credentials.json form a blocklist that stops Claude writing to them. That code denies access; it does not read those files.
Third-party MCP servers are recommendations, not bundled dependencies. The plugin ships no .mcp.json and declares no mcpServers. The table under Configuration is advisory, and /ork:setup asks before writing anything.
/plugin install ork
No tiering. No version confusion. Just one powerful plugin.
Not on Claude Code? Pull a starter 12 into any agent (Cursor, Codex, OpenCode, …) via skills.sh — do not install the whole firehose on day one. Picked from measured skills.sh installs on our listing plus the ork skills we actually retain:
| skill | why |
|---|---|
devops-deployment | most-installed cloud skill on our listing |
responsive-patterns | most-installed design skill on our listing |
architecture-decision-record | writing-bucket install leader |
ui-components | design install co-leader |
rag-retrieval | SE install leader |
agent-orchestration | agent-workflow install leader |
brainstorm | our most-retained ork skill |
expect | browser verification we run day to day |
auto | front-door router we actually invoke |
review-pr | ship-loop review we use |
fix-issue | debug loop we use |
commit | ship-loop commit (has a with/without receipt) |
npx skills add yonatangross/orchestkit -s devops-deployment -s responsive-patterns -s architecture-decision-record -s ui-components -s rag-retrieval -s agent-orchestration -s brainstorm -s expect -s auto -s review-pr -s fix-issue -s commit
All skills: npx skills add yonatangross/orchestkit. Live install totals stay on the skills.sh badge above, not hard-coded here.
Cursor loads Agent Plugins and Cursor plugins. This repo already ships the Agent Plugins manifest at plugins/ork/plugin.json. Add the GitHub repo as a Cursor marketplace (Settings → yonatangross/orchestkit), enable ork, then open a new chat. That is the same plugin Claude Code installs, not a five-skill fork.
It also ships 14 rules under the plugin's rules key, generated from src/rules/ and src/shared/rules/. They are agent-fetched, so a rule costs context only when its description matches the task.
Claude hook scripts are not registered for Cursor: they depend on ${CLAUDE_PLUGIN_ROOT} (orchestkit#293, closed). Cursor enforcement for HQ repos stays in the consuming project's .cursor/hooks.json.
"Include third-party Plugins" can leak SKILL.md from ~/.claude/plugins. That is not an install. Proof is the ork plugin id plus the full skill catalog.
Codex uses its own plugin format, skill picker, and standalone role configuration. Add OrchestKit's Codex marketplace, then install the small portable workflow pack:
codex plugin marketplace add yonatangross/orchestkit --ref main --sparse .agents/plugins --sparse plugins/ork-codex
codex plugin add ork-codex@orchestkit-codex
Restart Codex after installation. Invoke a workflow explicitly with $ork-brainstorm, $ork-explore, $ork-implement, $ork-assess, $ork-verify, or $ork-review-pr; their narrow descriptions also let Codex select the relevant workflow automatically.
The plugin intentionally ships roles as templates because Codex loads custom roles from ~/.codex/agents/, not from a plugin manifest. From an OrchestKit checkout, run this one-time, non-overwriting install:
plugins/ork-codex/scripts/install-codex-roles.sh ~/.codex/agents
It installs ork_explorer, ork_implementer, ork_reviewer, and ork_verifier; restart Codex before spawning them.
ork-mech profileFor mechanical work (renames, bumps, codemods, sweeps that end in a diff), install the shipped profile and run codex exec against it:
plugins/ork-codex/scripts/install-codex-profile.sh ~/.codex
codex exec --profile ork-mech "<task>" </dev/null
The profile is a FILE, not a snippet you paste into config.toml. Measured on codex-cli 0.153.4: --profile <name> layers $CODEX_HOME/<name>.config.toml over the base config, and a legacy [profiles.<name>] table left inside config.toml makes the same flag a hard config-load error. The installer refuses to run next to that table, and refuses to overwrite a profile you already have.
What it sets, as codex exec prints it in its own header:
approval: never
sandbox: workspace-write [workdir, /tmp, $TMPDIR] (network access enabled)
reasoning effort: high
It deliberately does not pin a model (pass -m) and does not use --dangerously-bypass-approvals-and-sandbox, which drops the sandbox entirely. Two contracts a TOML file cannot express, so they stay on the command line:
</dev/null. codex exec reads stdin even when a prompt argument is given. An inherited open pipe blocks the run with Reading additional input from stdin... and no timeout.--add-dir inside a git worktree. The writable roots are [workdir, /tmp, $TMPDIR]. A linked worktree's git common dir sits outside the workdir, so the first commit dies on index.lock. Add it: codex exec --profile ork-mech \
--add-dir "$(git rev-parse --path-format=absolute --git-common-dir)" \
"<task>" </dev/null
ref main in the marketplace source is a cached snapshot, not a tracker. On the 2026-09-08 audit machine codex plugin list showed 10.0.0-beta.5 while main was three releases ahead. After an OrchestKit release, update and check:
codex plugin update
codex plugin list | grep ork-codex # installed version
jq -r .version plugins/ork-codex/.codex-plugin/plugin.json # what main ships
The plugin ships a context7 MCP server in its own manifest (mcpServers in .codex-plugin/plugin.json, defined in mcp.json), so installing the plugin registers it. Confirm with codex mcp get context7. It is scoped to the only two tools context7 exposes, resolve-library-id and query-docs, and it uses the hosted HTTP transport rather than an npx stdio child, so it costs no extra process per Codex session.
Export a key before starting Codex. The plugin references the variable name and never stores the value, so no token is written to ~/.codex/config.toml:
export CONTEXT7_API_KEY_CODEX="<your-context7-api-key>"
Put that in your shell profile so every Codex session inherits it. Get the key from your own context7 account and keep the value out of the repository. If you store it in a secret manager, substitute your own vault and item names (with the 1Password CLI the reference is op://<vault>/<item>/credential), and cache the resolved value instead of re-reading the vault in every shell: each raw read is a separate unlock prompt.
Two behaviors worth knowing:
[mcp_servers.context7] in ~/.codex/config.toml wins, and the plugin's definition is ignored entirely (including its tool scoping). That is intentional: your own configuration is never overridden. Remove your entry if you want the plugin's.Invalid API key. A successful connection is therefore not proof of authentication.pi (0.85) reads the same SKILL.md format, and the repo now carries a pi manifest, so the package installs directly:
pi install git:github.com/yonatangross/orchestkit
That registers every skill. Add -l to write .pi/settings.json in the project instead of you
hooks/register.ts 107 lines1/**
2 * tool.list probe hook (R3).
3 *
4 * Answers every tool.list call with REMOVED_TOOL filtered out, and writes one
5 * JSON marker file per call so the probe script can count how many tool
6 * listings the engine built (main loop versus subagent) and see exactly what
7 * each listing offered before and after the filter.
8 *
9 * Marker location, first hit wins:
10 * 1. $.env.get('TOOL_LIST_PROBE_MARK')
11 * 2. $.session.cwd() + '/tool-list-mark'
12 * 3. $.env.get('TMPDIR') + '/tool-list-mark'
13 * 4. '/tmp/tool-list-mark'
14 * Each call appends '.' + a counter + '.' + Date.now() so every dispatch lands
15 * in its own file. A marker failure never breaks the answer.
16 *
17 * Note: $.env.get and every other $.noun.event call must take literal
18 * arguments; the loader lists the variables a module reads from the call
19 * sites, so indirection through a constant fails the load.
20 */
21
22const REMOVED_TOOL = 'Read';
23const MARK_BASENAME = 'tool-list-mark';
24
25type ToolInfo = { name?: string } & Record<string, unknown>;
26
27type Probe$ = {
28 env: { get: (name: string) => Promise<string | undefined> };
29 fs: { write: (path: string, text: string) => Promise<void> };
30 session: { cwd: () => Promise<string> };
31};
32
33type NextFn = ((e: unknown) => Promise<unknown>) & { origin?: string };
34
35let dispatchCount = 0;
36
37function asList(answered: unknown): ToolInfo[] | null {
38 if (Array.isArray(answered)) return answered as ToolInfo[];
39 if (answered && typeof answered === 'object') {
40 const v = (answered as { value?: unknown }).value;
41 if (Array.isArray(v)) return v as ToolInfo[];
42 }
43 return null;
44}
45
46async function markBase($: Probe$): Promise<string> {
47 try {
48 const mark = await $.env.get('TOOL_LIST_PROBE_MARK');
49 if (mark) return mark;
50 } catch {
51 // fall through to the next candidate
52 }
53 try {
54 const cwd = await $.session.cwd();
55 if (cwd) return `${cwd}/${MARK_BASENAME}`;
56 } catch {
57 // fall through to the tmp candidates
58 }
59 try {
60 const tmp = await $.env.get('TMPDIR');
61 if (tmp) return `${tmp}/${MARK_BASENAME}`;
62 } catch {
63 // fall through to the fixed path
64 }
65 return `/tmp/${MARK_BASENAME}`;
66}
67
68async function writeMark($: Probe$, record: Record<string, unknown>): Promise<void> {
69 try {
70 const base = await markBase($);
71 const stamp = `${dispatchCount}.${Date.now()}`;
72 await $.fs.write(`${base}.${stamp}`, `${JSON.stringify(record)}\n`);
73 } catch {
74 // Marker failure must not break the answer.
75 }
76}
77
78export function register(on: (event: string, hook: unknown) => void): void {
79 on('tool.list', async ($: Probe$, e: unknown, next?: NextFn) => {
80 dispatchCount += 1;
81 const answered = next ? await next(e) : undefined;
82 const seen = asList(answered);
83
84 if (seen === null) {
85 await writeMark($, {
86 removed: REMOVED_TOOL,
87 origin: next && next.origin ? String(next.origin) : null,
88 shape: 'unrecognized',
89 });
90 return answered ?? {};
91 }
92
93 const kept = seen.filter(t => t && t.name !== REMOVED_TOOL);
94 await writeMark($, {
95 removed: REMOVED_TOOL,
96 origin: next && next.origin ? String(next.origin) : null,
97 seen: seen.map(t => (t ? t.name : null)),
98 kept: kept.map(t => (t ? t.name : null)),
99 });
100
101 if (seen.length === 0) {
102 return answered;
103 }
104 return { value: kept };
105 });
106}
107