diff --git a/README.md b/README.md index 45582dc..e46d896 100644 --- a/README.md +++ b/README.md @@ -449,6 +449,19 @@ After `/plugin install opik-cipx@opik-enterprise`: | Skill | Purpose | |---|---| | `/opik-cipx:opik-cipx` | How opik-cipx works — architecture, the CLI, state layout, enable/disable, privacy/telemetry, and how to read `opik-cipx status`. Claude pulls it in on its own when you ask about opik-cipx or when spans stop reaching Opik; you can also call it directly. | +| `/opik-cipx:cost-intelligence-policy` | Which MCP servers your organization's cost policy denies for you, and how to turn one off or back on for yourself. Claude Code drops a denied server silently — it vanishes from `/mcp` and `claude mcp list` with no warning — so Claude pulls this in on its own when you ask where a server went. It reads the result of `opik-cipx mcp enable`/`disable` off the exit code, so a refusal is never reported back to you as a success. | + +## Commands (plugin) + +| Command | What it does | +|---|---| +| `/opik-cipx:cost-intelligence [thing]` | The receipt for "your Claude Code has been optimized". What cipx actually changed on this machine, what your organization's MCP policy actually blocks, what the last 30 days cost and where the tokens went, which cost policies are in effect and who set each one — and, for anything a policy turned off, the one route that turns it back on. Every figure is measured, none estimated. Pass an MCP server, skill, tool or settings key to ask about just that one thing instead. | +| `/opik-cipx:cost-intelligence-mcp` | The MCP policy table on its own: which servers your organization blocks, which are active, and whether you can change it yourself. | + +Both commands and the `cost-intelligence-policy` skill call `opik-cipx` as a bare command on +PATH, which the plugin supplies only from the release that adds the +`bin/opik-cipx` dispatcher — on an earlier plugin version they fail with +`command not found` or `unknown command`. ## Debugging diff --git a/commands/cost-intelligence-mcp.md b/commands/cost-intelligence-mcp.md new file mode 100644 index 0000000..064fc6e --- /dev/null +++ b/commands/cost-intelligence-mcp.md @@ -0,0 +1,26 @@ +--- +description: Show which MCP servers your organization's cost policy blocks, and whether you can change it +allowed-tools: Bash(opik-cipx mcp list) +--- + +Run `opik-cipx mcp list` and show its output. + +**Always show it. Every single time, without exception** — including when you +showed the same table one message ago and nothing has changed since. The user +ran this command to look at the table; "it is the same as before" is your +judgement about what they need, not an answer to what they asked. Saying +nothing changed and omitting it is the one failure this command cannot tolerate. + +Show the command's own output as it came back. Do not retype it, summarise it, +re-sort it, count it, or wrap it in a code fence — a fence strips the colour +that separates a blocked row from an active one at a glance, and retyping it +breaks the column alignment and asks the user to trust your transcription of +something they can already see. + +After the table you may add **one** short sentence, and only for something the +table cannot say — a server failing to connect for reasons unrelated to policy, +say. That sentence is an addition to the table, never a replacement for it. If +there is nothing to add, add nothing; the table alone is a complete answer. + +If the command fails, say so in one line and quote its message. Never describe +a policy you did not just read. diff --git a/commands/cost-intelligence.md b/commands/cost-intelligence.md new file mode 100644 index 0000000..5c6131d --- /dev/null +++ b/commands/cost-intelligence.md @@ -0,0 +1,369 @@ +--- +description: What cipx changed on this machine, what your organization's MCP policy blocks, what your last 30 days cost and where the tokens went, which cost policies are in effect for you and how to re-enable what they turned off — and, for org admins, the recommendations that would cut it further +argument-hint: [mcp server, skill, tool or settings key] +allowed-tools: Bash(opik-cipx cost-intelligence --markdown), Bash(opik-cipx cost-intelligence --markdown --commands), Bash(opik-cipx cost-intelligence --markdown --tool:*), Bash(opik-cipx cost-intelligence), Bash(opik-cipx mcp list) +--- + +Every session opens by telling this developer their Claude Code "has been +optimized" and sending them here. On its own that sentence is unverifiable. +Making it checkable is this command's whole job: what was actually changed on +this machine, what is actually blocked, what the last thirty days actually cost +and where the tokens went, which cost policies are actually in effect and who +set each one — each of them measured, none of them estimated — and, for +anything they turned off, the one route that turns it back on. + +What this session is costing right now is deliberately **not** here: those +readings are the status line's, which redraws them live on every frame. This +report is the one place they would have arrived stale. + +A `Recommendations` section appears only for organization admins — the backend +serves that list to admins only — so its absence is the ordinary case and never +something to flag, work around, or fetch another way. When it *is* there, the +developer reading it can change org policy from the terminal, under the rules in +"The Recommendations section" below. + +## When the user named something + +**`$ARGUMENTS` non-empty means they asked about one thing** — an MCP server, a +skill, a built-in tool, a Claude Code settings key. Run this and only this: + +``` +opik-cipx cost-intelligence --markdown --tool "$ARGUMENTS" +``` + +Paste what it prints and stop. Do not run the full report as well, do not run +`mcp list` "for context", and do not add the sections they did not ask for: they +asked why one thing is off and the answer is two lines. It exits 0 whether or +not the thing is off, and when it is not, that sentence is the answer too — not +a reason to go looking somewhere else. + +The name goes through **exactly as the user said it**. The command does its own +matching — case, a permissions rule's head, a dotted key's leaf — and reports +what it matched. Correcting the name yourself is how a user gets told about a +server they never asked about. + +## The full report + +With no argument, run both. Both are read-only. + +``` +opik-cipx cost-intelligence --markdown +opik-cipx mcp list +``` + +**`--markdown` is not optional.** Without it the report prints ANSI escapes, +which are right for a terminal and wrong for here: what you paste is rendered +as markdown, and an escape sequence in your reply is inert text. The colour +that tells a drifted value from a settled one would simply not arrive. +`--markdown` is the same report with that distinction carried by bold and by +words instead. + +If the user asks what they can run — what the commands are, what else cipx +does — add `--commands`: + +``` +opik-cipx cost-intelligence --markdown --commands +``` + +It appends a three-line list of the commands that exist. Leave it off +otherwise; it is the same on every machine every day, and it costs the report +the space its measured rows need. + +## Show the output, do not retell it + +**Paste each command's output into your reply as it came back, and stop.** + +Do not retype it, summarise it, re-sort it, count it, or wrap it in a code +fence. Both commands print finished, branded answers: aligned columns, and a +marked column that separates a blocked row from an active one, and a value +about to be reverted from one that is settled. A fence shows the markup instead +of rendering it, retyping breaks the alignment, and either way you have asked +the user to trust your transcription of something they could have read +themselves. + +After the two outputs you may add **at most one line**, and only for something +neither could say — that the daemon is not running, say. That line is an +addition, never a replacement. If there is nothing like that, add nothing; the +two outputs together are a complete answer. + +## The MCP table + +`opik-cipx mcp list` goes last, under the report — whose `Unused` rows are the +evidence for it and whose `Re-enable` groups name the same servers from the +other end. Show the table in full when either of these is true of what it +printed: + +- any row reads `blocked`, or +- it carries a `Most recent change` block. + +Otherwise replace it with **one line** — that no MCP server is blocked, and +that `/cost-intelligence-mcp` shows the table — and spend the space on the +other two sections. A screen of green `active` rows is the longest thing in +the reply and the only part of it that is not news; a blocked row is the +opposite, and it must never be the thing that got cut for room. + +## The status line, when they ask about it + +The live readings this report deliberately does not print are the status line's, +and this is where the question about them arrives. `opik-cipx sync` points +Claude Code's `statusLine` at `opik-cipx statusline` on every run, so every +developer with the plugin has the row whether or not they installed anything — +which means the row showing `◕ 42% · $1.87` is cipx's, and "what is this thing +in my status line?" is a question every session can be asked. + +**Describe the row, do not send them here for it.** This report cannot answer +it: the readings are drawn on every frame and appear nowhere in what the command +prints. What the segment carries, left to right after the `◕`: context depth as +a percentage of the window, the session's cost so far, and — when they are worth +the columns — the five-hour and seven-day rate-limit windows as `5h 12%` / +`7d 71%`. At most one alert is ever added: `compact` (context nearly full), +`200k+` (past 200k tokens, where a token costs more), `limit` (a rate window +nearly spent) or `cold` (the prompt cache went cold). A recently applied org +setting can appear last, as `autoCompactWindow 2h ago`. + +What else is true, and worth having straight before answering: + +- **It did not replace their own status line.** cipx captured their command to + `~/.opik-cipx/statusline-wrap.json` and runs it underneath, every row of it; + its own segment takes a row below theirs. If their line vanished, that is a + bug and not the design — say so rather than explaining it away. +- **Two switches, and they do different things.** + `OPIK_CIPX_STATUSLINE_DISPLAY=0` hides the segment and leaves the sensor + recording, so the row goes back to being entirely theirs. + `OPIK_CIPX_STATUSLINE=off` opts out of the feature: no segment, and the next + sync puts their own status line command back. Neither switch changes anything + in this report. +- **Never send them to edit `statusLine` in `~/.claude/settings.json`.** Sync + re-claims the key on the next prompt, so the edit disappears and looks like + cipx fighting them. Name a variable instead. +- **An organization can push the status line through managed settings**, which + outranks their file. There the switches above still hide the segment, but + nothing local hands the key back — the org owns it. + +The report says nothing about any of this — the whole section above is answered +from here, not from its output. In particular, do not go reading +`~/.opik-cipx/statusline.json` for numbers to quote: what is in that file is +whatever the last frame wrote, it may belong to another Claude Code window, and +pasting it hands the user a stale reading they will act on. + +## The spend section + +`Last 30 days` is the report's second section and the first that came off the +network. It opens with `Spend` — what the window actually cost, split into the +share of a seat, whatever went past the plan, and whatever was billed straight +to the API — then says where the tokens went (`Input`, `Output`), which models +and repositories they went on (`Models`, `Repos`), and finally, under `Unused`, +the MCP servers, skills and built-in tools that were billed all month for being +available and were barely used. + +**Three different dollars appear in that section and they are not the same +figure.** The report labels each where it prints it, and those labels travel +with the figure or the figure does not get pasted: + +- `$23.74 total · seat $8.34 · over-plan $12.00 · API $3.40` is money. +- `≈ $12.60 at list` is what those tokens *would* cost at API rates. Under a + subscription seat it is a shadow price nobody was invoiced, and it routinely + dwarfs the real total three rows above it. +- `$9.99 cash` on a `Models` or `Repos` row is marginal money only — over-plan + plus API. `in-plan` there means a seat covered it, which is a measurement and + not a missing figure: never restate it as `$0.00`. + +Those `Unused` rows are the evidence behind a `blocked` row in the MCP table, +which is why the two are printed next to each other. Show them whenever they +are there. They are the one part of this report that names something the +developer could turn off today, and the token figures beside each name are the +whole argument for doing it. + +The section can also print a single quiet line instead — that no backend is +configured, that the credentials were refused, or that it could not be reached +in time. That line is the answer. Show it as it came and add nothing: there is +no cached composition anywhere, and the section says so precisely because it +has nothing to report. + +## Savings by policy source, and Re-enable + +`Savings by policy source` is the report's third section: the cost policies +actually in effect for this developer, split into `Org` — what their +organization set — and `You` — what they set for themselves. Each block leads +with how many policies it holds, how many of those the backend could put a +price on, and what those add up to. + +**`n/a` is an answer, and it is not zero.** It means the backend could not +observe that policy's saving from spend recorded after it was applied. Never +report it as $0.00, never call it "no saving", and never add up a column that +has one in it. + +On a deployment whose cost-api does not serve that endpoint yet, the section is +replaced by `Policies applied · source unknown` plus a sentence saying a +cost-api update is what adds the split. Show it as it came. The rows are real — +they are what cipx applied on this machine — and the missing half is the source, +which is exactly what the heading says. + +`Re-enable` is the fourth: for everything policy turned off, the one route that +turns it back on, grouped by route. `Locked` means an admin's, and the line says +plainly that editing `~/.claude/settings.json` will not stick. `Org` means a +default the developer may override for themselves. `Yours` means they turned it +off in their own Opik preferences. + +**Never invent a sixth route.** Do not suggest editing settings.json for a +`Locked` row, do not suggest a `--dangerously` flag, do not offer to change the +policy yourself, and do not paste a preferences URL — the report names the page +in words on purpose, because a raw URL is the one thing on the line that gets +truncated. + +## The Recommendations section + +It is the last section, and it is the one that only an admin gets. Each row is +an id, what applying it would still save, and what it changes — plus +`quality trade-off` where the change can affect the answers Claude gives, and +`drifted` where it was applied and has since been undone. + +**The section's presence is the only admin test there is.** The report asked the +backend with this developer's credential and rendered the section on a 200; a +member got a 401 or a 403 and no section. Never claim a role the report did not +show you, and never go looking for the list another way when it is absent. + +When the section IS there, you may offer — in one line, after the outputs — +`opik-cipx policy apply ` for one row the user is asking about. Before +running it you must say, in your own words, that it applies to **everyone in the +organization**, that it is **re-applied on every policy check** so a developer's +own `settings.json` edit does not survive, and — if the row carries it — that it +is a **quality trade-off**. Then wait for an explicit yes. Preparing the command +is not permission to run it. + +When the section is NOT there, the route stays what the report prints: the Opik +Cost savings page, or their admin. Do not offer a `policy` command to a +developer whose report has no Recommendations section — it would exit 3. + +## Length + +**28 lines.** A `--tool` answer is its own budget: **whatever it printed, and +nothing else.** + +That number is a ceiling rather than a target — a saturated report runs to +fifty-five lines, and most of what it adds is cuttable. If the two outputs +together run past the budget, cut in this order and stop the moment it fits: + +1. the `Commands` section — reference, not news: it is the only part of the + report that reads the same on every machine every day, and re-running with + `--commands` brings it straight back +2. the `Models` and `Repos` rows — a leaderboard is where the money went, not + something to do about it +3. the `Input` and `Output` lane rows — where the tokens went is context, and + the `Unused` rows below them are the same section's actionable half; cut the + context and keep the action +4. the MCP table — down to the one-line form above +5. the two-line enforcement sentence — down to "Your organization sets these" +6. the per-entity `↳` rows under a policy — the policy row above them stays +7. policy rows past the first two in each block — the rest as a count +8. `Unused` rows past the first two — last out, and the rest as a count + +Cutting means omitting whole lines. It never means rewording a line to make it +shorter: the moment you retype one you are back to transcription. `Commands` +comes out whole or not at all — half a list of commands reads as the whole one. + +**Never cut a `Re-enable` group.** It is the only part of the reply that says +what to do rather than what is true, and a route the user cannot see costs them +the change they came to make. The `Commands` section is the exception and only +the exception: nothing in it is attached to a reading, which is why it is first +out. + +## Never + +- **Never compute or estimate a figure of your own, in any form** — a saving, + a total, a projection, a monthly rate, a per-row sum. There is no price table + anywhere in cipx, and a number invented to fill a gap would discredit every + measured number beside it. +- **Every `$` the report printed may be pasted verbatim, and only verbatim.** + Each was priced server-side by the spend backend, which owns the price table. + Copy them. Do not add them up, scale them to a month, or turn one into what + it "would save". +- **`tokens idle` and `tokens of schema` are token counts, and they are not + the same count.** `tokens idle` is what the backend attributed to nobody + using the entity; `tokens of schema` is everything the entity was billed for + being available, on a row where the backend reported no narrower figure. Keep + whichever word the report used, and never multiply either by a price. +- **`at list` travels with the figure it is attached to.** Those dollars are + what the tokens would cost at API rates, which under a subscription seat is + not money anyone was invoiced — and the section now prints real money three + rows above them. Never present a list figure as this developer's bill, never + drop the words that say which it is, and never compare the two. +- **`in-plan` and `n/a` are readings, not blanks.** `in-plan` means a seat + covered that row; `n/a` means the backend could not measure that saving. + Neither is $0.00, neither is "nothing", and neither may be filled in. +- **Never say denying an MCP server keeps tool *definitions* out of the + context.** Claude Code defers MCP schemas behind tool search and fetches them + on demand, so they were never in the request; measured on live traffic, a + denial removes the server's entry from a list of tool names. What is true is + what `mcp list` already prints: a blocked server never starts, so none of its + tools exist in the session. +- **Never supply a reading the report did not print.** It does not report what + this session is costing, and that is not a gap to fill: those readings are + the status line's, and it redraws them every frame. Do not go looking for + them in `~/.opik-cipx/statusline.json` — what is in that file is whatever the + last frame wrote, it may belong to another Claude Code window, and pasting it + hands the user a stale number they will act on. The same holds for the + composition: if that section printed one quiet line, do not go and query the + backend yourself. The report asked with the credential that scopes the answer + to this developer; anything you fetch another way is scoped to somebody else + or to everyone. +- **Never guess a Claude Code default the report did not print.** Every default + it shows was read out of the shipping CLI binary. A key with no default beside + it is one nobody has verified, and it stays that way in your reply too. +- **Never imply the applied settings are editable.** They are re-applied on + every policy check, including one that finds nothing changed. +- **Never name an `opik-cipx` command the output did not print.** The list + under `Commands` is generated from the commands that actually exist in the + binary that just ran. One you added from memory is one this developer's + binary may not have, and they will find that out by typing it. +- **Never run an `opik-cipx policy` write on your own initiative.** `apply`, + `set` and `unset` change policy for the whole organization. Reading the + Recommendations section is not a mandate to act on it: name the command, say + what it does to everyone, and wait for the user to say yes. An org-wide + change nobody asked for is the one mistake in this report that other people + find out about. +- **Never present `opik-cipx mcp enable` as free.** The report prints its + consequences on the same line — it asks the organization, it does nothing + until the next session, and it exits 3 when an admin has locked the policy. + Those travel with it or it does not get named. + +## When the command is not there + +Both commands run whatever `opik-cipx` resolves to on PATH, and a machine with +a working, capturing cipx can still have nothing under that name. A plugin +install normally does provide it: the plugin ships a `bin/opik-cipx` dispatcher +and Claude Code puts every plugin's `bin/` on PATH, so `opik-cipx` resolves to +the build for this platform. A curl install does not — the installer only +prints a suggestion to add `~/.opik-cipx/bin`. Two messages mean the command is +missing, and not that cipx is broken or absent: + +- **`command not found`, or `no installed binary found for …`** — nothing on + PATH answers to `opik-cipx`. Say the report cannot be run here, not that + cipx is not running; the session hook invokes the binary by absolute path + and never needed PATH to begin with. For a curl install the fix is to put + `~/.opik-cipx/bin` on PATH. For a plugin install it means the dispatcher is + not there — a plugin version older than it, or no build for this + `-` in the plugin's `bin/` — and updating the plugin is the fix. +- **`unknown command "cost-intelligence"`** (or `"mcp"`) — the binary on PATH + is older than the report. Name the version it printed if you have it, and + say that upgrading the plugin is what adds the command. +- **`unknown flag: --markdown`** (or `--commands`) — same cause, one version + nearer: the command exists, the flag does not. This is the one case where + running it a second time is right. Re-run it bare, once, and show what comes + back; it will carry escape sequences this surface cannot render, so say in + one line that the report is from an older binary and reads plainly here. + +Quote the failing message once, in one line, and stop. Do not retry it in +another form — the one exception is the rejected flag above, and that retry is +the same command with a flag removed, never a different command. Everything +else interrupts the user for permission to learn what you already know. + +**When a command fails, report nothing from its section.** Everything in it +comes from the command that just refused to run — there is no cached copy to +fall back on, and the session intro's "has been optimized" is the claim under +test, never evidence for it. A report reconstructed from anywhere else is +precisely the unverifiable sentence this command exists to replace. + +If only one of the two fails, run and show the other; a missing MCP table does +not spoil a cost reading, or the reverse. Say in one line which half is +missing and why, then show the half you have. diff --git a/hooks/hooks.json b/hooks/hooks.json index ac533fd..e0076aa 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,6 +1,7 @@ { "hooks": { "SessionStart": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/opik-cipx-hook-launcher session-start"}]}], - "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/opik-cipx-hook-launcher user-prompt-submit"}]}] + "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/opik-cipx-hook-launcher user-prompt-submit"}]}], + "Stop": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/opik-cipx-hook-launcher stop"}]}] } } diff --git a/skills/cost-intelligence-policy/SKILL.md b/skills/cost-intelligence-policy/SKILL.md new file mode 100644 index 0000000..d47a94b --- /dev/null +++ b/skills/cost-intelligence-policy/SKILL.md @@ -0,0 +1,231 @@ +--- +name: cost-intelligence-policy +description: Cost Intelligence policy — inspect and change which MCP servers your organization's cost policy denies for you, and, for an organization admin, change the org's cost policy itself. Use when the user asks why an MCP server or its tools are missing, asks to turn one off or back on for themselves, asks to undo their own override or follow the org again, or asks what their organization has blocked — e.g. "why is Notion gone", "turn the notion MCP back on", "disable slack for me", "undo what I did to slack", "reset my MCP overrides", "what's blocked?", "what else did it block?" — and when an admin asks to change it for everyone: "make Sonnet the default for the team", "apply that recommendation", "compact earlier org-wide", "stop pushing the default model". +allowed-tools: Bash(opik-cipx mcp:*), Bash(opik-cipx policy recommendations) +--- + +# Cost Intelligence policy + +An organization can deny MCP servers through Opik Cost Intelligence. Claude Code +removes a denied server silently — it disappears from `/mcp` and +`claude mcp list` with no warning — so this skill is usually how the user finds +out what happened. + +## Commands + +``` +opik-cipx mcp list # status table of every MCP server +opik-cipx mcp disable # deny a server for this user +opik-cipx mcp enable # stop denying one for this user +opik-cipx mcp reset # drop this user's own choice, so the org's applies +opik-cipx mcp reset --all # drop every one of them at once +``` + +Pass the server name as the user said it. The command does its own matching +against the MCP servers configured on this machine, and handles exact names, +the `mcp__notion__search` tool-id form, and partial names. **Do not guess a +name, correct one, or pick between candidates yourself** — the command reports +an ambiguity so a human can resolve it, and overriding that is how a user gets +told the wrong server was changed. + +`reset` is the exception to the matching rule above: a name it cannot match is +sent as typed rather than refused, because the choice being dropped can be for +a server the user no longer has configured. So exit 4 never comes back from +`reset` — a genuine typo comes back as exit 7, "nothing to reset". + +## `disable` is not the undo for `enable` + +`enable` and `disable` both store **the user's own** choice about a server, on +their account. That is why neither undoes the other: `disable` after `enable` +stores "I deny this too", which is a second opinion that keeps diverging from +the organization every time the organization changes its mind. The user's +choice also re-applies on its own — an `enable` survives a lock and comes back +the moment the admin unlocks again. + +`reset` removes the user's choice, so the organization's answer applies to them +again. It is the only way to hand a server back. + +Suggest it when the user says any of: + +- "put it back the way the org has it", "stop overriding it", "follow the org" +- "why did that server come back after the admin locked it?" — because their + own `enable` is still on their account, and only `reset` clears it +- "undo what I did to slack" — `mcp reset slack`, never `mcp disable slack` +- "reset all my MCP overrides" — `mcp reset --all` + +Do not reach for it when they want a server **off**: that is `disable`. Reset +is not "turn it off", it is "I have no opinion". If the organization does not +deny that server, resetting leaves it on. + +## Show the output, do not retell it + +**Every one of these commands prints a finished, branded answer. Paste it into +your reply verbatim and stop.** + +`mcp list` in particular is a table — one row per server, aligned columns, +saying what is blocked, what is active, and whether the user can change it. +Turning that into sentences is a downgrade in every direction: it is longer, +it loses the alignment that made it scannable, it re-sorts rows the user was +about to read, and it forces them to trust your transcription of something +they could have read themselves. Never do it. Do not summarise the table, do +not list its rows as prose, do not lead with a count. + +Show it every time it is asked for, including when you showed the same table a +message ago and nothing has changed. "It is unchanged" is your judgement about +what the user needs rather than an answer to what they asked, and it leaves +them reading your memory instead of their policy. + +After the output you may add **at most one line**, and only for something the +table cannot say — a server that failed to connect for reasons unrelated to +policy, say. That line is an addition to the output, never a replacement for +it. If nothing like that is true, add nothing. + +## What the user already saw + +A policy change prints a short notice when their session starts. It names **no +server** — only that policy changed, and where to look. So the user genuinely +does not know which servers moved, and asking them which one they mean will not +work. + +Your context does carry the full change, but a change is a **diff**: it says +what just moved, not what is blocked. A server denied a month ago appears in +neither. + +So `opik-cipx mcp list` is the answer to both "what changed?" and "what's +blocked?" — it prints the most recent change and the current policy together. +Run it rather than answering from the diff in your context alone. + +## Reading the outcome + +**The exit code is the result. The text is for the user.** Never describe an +outcome the exit code does not support. + +| Exit | Meaning | What to tell the user | +|---|---|---| +| 0 | Changed | It worked, **and it applies from their next session** — pass on the restart command the output prints, verbatim (see Timing) | +| 1 | The command line was wrong | Nothing was sent; `reset` refuses `--all` beside a server name, and refuses neither | +| 3 | Locked by their admin | It did **not** change, and they cannot change it here. A lock blocks `reset` too — reverting is not a way around one | +| 4 | No such server configured | It did **not** change; likely a typo, offer `opik-cipx mcp list`. `reset` never returns this | +| 5 | Several servers match | It did **not** change; show the candidates and ask which | +| 6 | Not enabled for this org | It did **not** change; they can change it in the Opik dashboard | +| 7 | Already in that state | Nothing changed, and nothing needed to. From `reset`: they held no choice of their own about that server, so the org's policy was already what applied | +| 8 | Could not read an MCP config | It did **not** change; the name cannot be matched safely | +| 9 | Backend refused or unreachable | It did **not** change | + +Any non-zero exit means **nothing changed**. Say so plainly. Do not soften a +refusal into "done" or "that should be sorted now" — the user will act on it, +find the server still missing, and trust nothing this skill says afterwards. + +The command prints the reason to stderr. Quote it rather than inventing +wording. + +## Do not restate the mechanics + +Underneath the output, every run is also true of every other run: that a denied +server was never launched, that the override is per-user, that an admin could +re-push the policy, that a change waits for the next session. A user who reads +that once has read it forever. Volunteering it each time turns a one-line +confirmation into a lecture and buries the detail that was specific to this run. + +If they ask why, explain then — and reach for `opik-cipx mcp list` rather than +answering from memory. + +## Timing + +A change made with `disable`/`enable`/`reset` **never** takes effect in the +running session. A denied server's process was never started, so nothing can +bring it back mid-conversation. Always tell the user how to restart, and never +imply the tools are available now. + +### Relay the restart command verbatim + +The output ends with a command on a line of its own — `claude --resume `, +or `claude --continue` when it could not tell which session this is. **Copy +that line into your reply exactly as printed.** Do not paraphrase it as +"restart Claude Code", do not shorten it, and do not substitute a command of +your own: the id is this session's, resolved from the working directory, and it +is the only thing that brings the user back to the conversation they are having +with you. "Restart Claude Code" reads as "start over", and a user who acts on +it loses this session. + +`--continue` is the weaker form, and when it is what was printed, say the +directory part too: it takes the most recent conversation in the directory it +is run from, so it only means this session when it is run here. + +What the user does with it: `/exit` ends the session (Ctrl+C twice does the +same), then the command reopens it with its history intact — it is not a new +conversation, and nothing said so far is lost. Say that if they hesitate; it is +the reason the command is worth typing rather than just relaunching `claude`. + +The notice at the top of a session is the other side of this: that change had +already taken effect before the session started. Its servers are off (or back) +right now, so do not tell the user to restart for it. + +## Scope + +This changes the policy for **this user only**, and only if their admin left +the setting unlocked. It cannot change anyone else's, and it cannot override a +lock. If the user wants a locked setting changed, they need their admin — or +they may be one themselves, which is the section below. + +## If the user is an admin + +`opik-cipx policy` changes the policy for the **whole organization**. It is a +different command group from `mcp` for a reason: `mcp` stores one developer's +exception, and every verb here lands on everybody. + +``` +opik-cipx policy recommendations # what the org could still do, and what it would save +opik-cipx policy apply # apply one, org-wide +opik-cipx policy set [--lock|--unlock] +opik-cipx policy unset +``` + +**How to know whether they are an admin: run the report and look.** If +`opik-cipx cost-intelligence` prints a `Recommendations` section, the backend +served this developer the admin list and they are one. If it does not, they are +not — the section is absent for a member, silently and by design. There is no +other test, and **never** assert or assume it: the client cannot tell, only the +backend can, and `opik-cipx policy recommendations` refusing with exit 3 is the +same answer arriving the expensive way. + +For a **non-admin**, nothing changes about this skill: their route to an org +setting is their admin, or the Opik page the report names. Do not offer them a +`policy` command; it would exit 3 and read as a broken tool. + +### Never run a `policy` write on your own initiative + +`apply`, `set` and `unset` are org-wide writes. They are deliberately **not** in +this skill's allowed tools, so each one goes through the user's own permission +prompt — that prompt is the consent, and it is not something to work around. + +Before running one, say all three of these in your own words and **wait for an +explicit yes**: + +1. **It applies to everyone in the organization**, not just to them. +2. **It is enforced on every policy check** — a developer editing their own + `settings.json` gets it re-applied, and there is no per-developer opt-out for + a setting (unlike an MCP denial, which `mcp enable` can except). +3. **The risk tag, if the row carries one.** `quality trade-off` on a + recommendation means applying it can change the answers Claude gives. If the + row has it, say so before asking; do not soften it. + +"Apply the model one" is a request to *prepare* the command, not permission to +run it. A user who has not been told points 1–3 has not agreed to them. + +### Reading the outcome + +Same exit codes as the `mcp` commands, same rule: **the exit code is the +result**, and any non-zero means nothing changed. Two of them mean something +different here: + +| Exit | Meaning | What to tell the user | +|---|---|---| +| 3 | Refused | Two sentences, and they are not the same news. "needs the plugin to authenticate you" means the terminal write path is not open on this deployment yet — the change can still be made on the Opik Cost savings page. "does not list you as an admin" means this developer is not an admin; their admin can do it | +| 4 | Unknown recommendation, or the backend refused the change | Quote the message. For a name, offer `opik-cipx policy recommendations`. For a setting the backend rejected, the message is the backend's own validator — never substitute a list of your own | +| 5 | Several match | Show the candidates and ask which; never pick | +| 7 | Already in effect | Nothing changed and nothing needed to | + +After a successful write, the confirmation says when it lands. Every developer +also learns about it from their own session-start notice, the admin included — +so do not suppress or duplicate that; it is how they see it took.