surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| @@ -34,10 +34,10 @@ | ||
| 34 | 34 | | `tool.describe`, `command.describe`, `agent.offer`, `agent.spawn` | When the subject's pinned `e.provider.tier` is `prepend` or `append` (a policy-installed plugin, the managed folder, a policy MCP server), continue past the user tier; a subject provided by `user`, `builtin` or `core` passes. | |
| 35 | 35 | | `tool.register` | A caller in `prepend` or `append` continues past the user tier. A `user`-tier caller is refused by name while managed settings hold `allowedMcpServers` (set at all, empty included). With no allowlist, one is still refused whose tool, called `mcp__<plugin>__<tool>`, would read as a tool of a server the organization delivers: a `managedMcpServers` entry, or a server of the enterprise `managed-mcp.json`, which the policy read names under that key. So is one whose name reads as a plugin the organization seats itself (`prependPlugins`, `appendPlugins`, or an id managed `enabledPlugins` turns on, by `true` or by a version list, as the engine reads an entry), or as an MCP server such a plugin's own manifest declares, which the engine spells under the plugin's scope (`mcp__plugin_<plugin>_<server>__`). A `user`-tier plugin keeps its own name exactly when the id the engine recorded for it (`<name>@<marketplace>`: the settings key the loader resolved, never the plugin's manifest) is one managed `enabledPlugins` turn on, compared as the engine compares ids (NFC, then lower case). That is how an organization's own plugin registers its tools where the engine leaves it in the `user` tier: only a copy read in place from a local folder marketplace that managed settings name is vouched for, so a plugin from a remote marketplace (git, GitHub, npm) can never be vouched for in this release, and neither is one a version list turns on. For an organization that means such a plugin runs among a person's plugins in the chain, not ahead of them, and keeps its tools by this exemption alone. A plugin loaded with `--plugin-dir` is always `<name>@inline` and never matches; nor does a same-named plugin from another marketplace, one whose name only folds to the organization's, or an id policy lists only in `prependPlugins` or `appendPlugins`. A person who registers a marketplace of their own under the organization's marketplace name gets an equal id, and so the exemption: it gives that plugin what every plugin had before this hold existed, and `strictKnownMarketplaces` in managed settings closes it today by allowing no other marketplace source. The mod hears a plugin's id at its admission, so after a load that seats the mod afresh while other plugins stay seated (a load that folds again, as a policy change mid-session can cause) the organization's own plugin is refused its name until the session restarts. A plugin's own MCP servers are connected by the engine from its manifest, not registered through `tool.register`, so they are unaffected. An id's name is everything before its last at-sign, as the engine parts ids, so a scoped `@scope/name@npm` counts. A `managed-mcp.json` that is present and cannot be read whole names nobody: the policy read marks it (`managedMcpServersUnreadable`) and every such registration is refused while it stands, its servers never taken for none; the only other reader of the mark is `tool.list` (the next row), so a person's plugins still load and still set what is not pinned. A server the file names and the engine dropped with a warning is still named; a warning about a server that loads is no failure. Names are compared folded, as a model reads them (spelled as the engine spells a name inside a tool's name, every character outside letters, digits, `-` and `_` a `_` and nothing normalized; then lower case, every run of `-` and `_` one `_`; a seated plugin counts under its name composed and decomposed, since the engine spells its tools from its manifest's own form): a plugin named as the server in any case or with other separators, one whose name and tool together spell it, and one whose tool's own name folds to nothing, is refused. A server whose name folds to nothing (no letter or digit in it) cannot be told from any name, so while the policy names one every `user`-tier registration is refused. The engine pins the tool's name at this event (a hook beneath that hands on another name fails, and the chain goes on without it), so the name judged is the name registered. Otherwise it passes. | |
| 36 | 36 | | `tool.list` | The tools of the organization's managed MCP servers, of the plugins it seats itself and of the MCP servers those plugins' own manifests declare (told by the name whole, as the engine spells it inside a tool's name: `mcp__<server>__`, never by a fold, so another server whose name only begins like the organization's is not taken for it; a seated plugin and its own servers, under its scope `mcp__plugin_<plugin>_`, fold to fold as the registration check compares, so policy may list its id in another letter case, and another plugin whose name begins with the seated one's and a `-` or `_` reads the same) are listed as the organization's tiers listed them; every other tool as the user tier left it. Where the server file cannot be read, every `mcp__` tool counts as the organization's. With no policy to read, or a refusal from either listing, the organization's listing stands whole. | |
| 37 | -| `tool.check` | A deny, and an ask a settings rule, a classic hook or an organization's ceiling on the tool stands behind, hold whole over the user tier: when a person's plugin rewrote the verdict it was handed without tightening it (a more permissive decision, or the same decision under another reason, rule or hook), the dispatch is run again past the user tier, and unless the chain's answer is the stricter, that verdict, if it is a deny or names the rule, the hook or the ceiling behind it, is the answer as the engine gave it. See [Deny rules hold](#deny-rules-hold). Every other verdict passes as the chain left it. | | |
| 37 | +| `tool.check` | A deny, an ask a settings rule, a classic hook, a setting's own check or an organization's ceiling on the tool stands behind, and an ask a host answers in the person's place, hold whole over the user tier: when a person's plugin rewrote the verdict it was handed without tightening it (a more permissive decision, or the same decision under another reason or with what it names changed), the dispatch is run again past the user tier, and unless the chain's answer is the stricter, that verdict, if it is a deny or names what stands behind it or the host it is put to, is the answer as the engine gave it. See [Deny rules hold](#deny-rules-hold). Every other verdict passes as the chain left it. | | |
| 38 | 38 | | `ui.log` | A line a plugin in `prepend` or `append` logs, this one's included, continues past the user tier: no user hook rewrites or drops it, and none runs inside the hook that logged it. Every other line passes. | |
| 39 | 39 | | `env.get` | A read by a plugin in `prepend` or `append` continues past the user tier: no user hook answers what the organization's own plugin reads. Every other read passes. | |
| 40 | -| `env.set` | A write by a plugin in `prepend` or `append` continues past the user tier, whatever the variable: no user hook rewrites or drops what the organization's own plugin sets. A variable managed settings set in `env` (its name in any case) is pinned, and so are `CLAUDE_CODE_PROCESS_WRAPPER` where they name a launcher with `processWrapper` and, always, the nine variables that move where policy and sign-in are read from or how policy's tiers compose (`CLAUDE_CODE_MANAGED_SETTINGS_PATH`, `CLAUDE_CODE_REMOTE_SETTINGS_PATH`, `CLAUDE_CODE_MOCK_REMOTE_SETTINGS`, `CLAUDE_CODE_DISABLE_ADMIN_ENV_UNION`, `CLAUDE_CONFIG_DIR`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `USE_LOCAL_OAUTH`, `USE_STAGING_OAUTH` and `CLAUDE_LOCAL_OAUTH_API_BASE`, which choose the server sign-in goes to; all nine on every build): setting or unsetting it, a `user`-tier caller is refused by name and a built-in's write continues past the user tier. Any other variable is a person's plugin's to set. With no policy to read, every variable counts as pinned. `allowModsToOverrideDenyRules` does not unpin. See [Pinned variables](#pinned-variables). | | |
| 40 | +| `env.set` | A write by a plugin in `prepend` or `append` continues past the user tier, whatever the variable: no user hook rewrites or drops what the organization's own plugin sets. A variable managed settings set in `env` (its name in any case) is pinned, and so are `CLAUDE_CODE_PROCESS_WRAPPER` where they name a launcher with `processWrapper` and, always, the nine variables that move where policy and sign-in are read from or how policy's tiers compose (`CLAUDE_CODE_MANAGED_SETTINGS_PATH`, `CLAUDE_CODE_REMOTE_SETTINGS_PATH`, `CLAUDE_CODE_MOCK_REMOTE_SETTINGS`, `CLAUDE_CODE_DISABLE_ADMIN_ENV_UNION`, `CLAUDE_CONFIG_DIR`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `USE_LOCAL_OAUTH`, `USE_STAGING_OAUTH` and `CLAUDE_LOCAL_OAUTH_API_BASE`, which choose the server sign-in goes to; all nine on every build), and, on every build too, ten variables that can load code into a child the engine starts (`LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, `DYLD_FRAMEWORK_PATH`, `BASH_ENV`, `ENV`, `ZDOTDIR`, `NODE_OPTIONS`): setting or unsetting it, a `user`-tier caller is refused by name and a built-in's write continues past the user tier. Any other variable is a person's plugin's to set. With no policy to read, every variable counts as pinned. `allowModsToOverrideDenyRules` does not unpin. See [Pinned variables](#pinned-variables). | | |
| 41 | 41 | | `plugin.register` | A hooks module in the `user` tier (one a person installed, named with `--plugin-dir`, or keeps in their mods folder) is refused while managed settings set this plugin's `allowManagedModsOnly` option; otherwise it passes. Modules in `prepend`, `append` and `builtin` are never asked about. | |
| 42 | 42 | | everything else | Passes: `prompt.submit`, `turn.*`, `tool.call`, `command.run`, `command.register`, `command.list`, `agent.register`, `config.*`, `prompt.fill`, `prompt.suggest`, `prompt.edit`, `prompt.read`, `session.*` but `session.append` and `session.compact`, `ui.*` but `ui.log`, `fs.*`, `http.fetch`, `process.run`, `store.*`, `clock.*`, `model.*`, `mcp.call`, `audio.*`, `agent.list`, `engine.create`. | |
| 43 | 43 | |
| @@ -92,8 +92,9 @@ | ||
| 92 | 92 | |
| 93 | 93 | `allowModsToOverrideDenyRules`: the plugins a person installs may answer |
| 94 | 94 | over a settings deny rule on `tool.check`, as they could before this plugin |
| 95 | -held deny rules, and over every other deny, an ask rule or a classic hook's | |
| 96 | -ask with it: the one option covers all that holds there. Off unless it is the | |
| 95 | +held deny rules, and over every other deny, an ask rule, a classic hook's | |
| 96 | +ask, a setting's own ask or an ask a host answers with it: the one option | |
| 97 | +covers all that holds there. Off unless it is the | |
| 97 | 98 | literal `true`; an option that reads as unset leaves them holding. See |
| 98 | 99 | [Deny rules hold](#deny-rules-hold). |
| 99 | 100 | |
| @@ -124,7 +125,8 @@ | ||
| 124 | 125 | On `tool.check` any hook may answer any verdict, so a plugin a person |
| 125 | 126 | installs to stop the permission prompts (`() => ({ decision: "allow" })`) |
| 126 | 127 | would also lift a deny rule, an ask rule or a `PreToolUse` hook's ask, a |
| 127 | -managed one included. Where this plugin is seated it does not: | |
| 128 | +managed one included, the read block's ask on a shell command, and the ask a | |
| 129 | +host was to answer. Where this plugin is seated it does not: | |
| 128 | 130 | |
| 129 | 131 | - The hook first runs the chain as it is. If no link that may hold a |
| 130 | 132 | person's plugin rewrote the verdict handed up to it (a more permissive |
| @@ -145,25 +147,77 @@ | ||
| 145 | 147 | changes nothing: the rules are evaluated in this run. The two runs differ |
| 146 | 148 | by the user tier alone, so where they differ and the chain's answer is not |
| 147 | 149 | the stricter, the user tier rewrote it. A deny is returned in place of the |
| 148 | - chain's answer whatever decided it; an ask, when it names what decided it: | |
| 149 | - a settings rule (`rule`), a classic hook (`hook`) or an organization's ask | |
| 150 | - cap on the tool (`ceiling`). It is returned whole, | |
| 151 | - so the reason this event answers, which a person reads in the prompt and the | |
| 152 | - engine writes into a refusal, is the engine's, and the rule or hook is still | |
| 153 | - named. (The refusal then enters the conversation as a tool result, a row a | |
| 154 | - person's plugin may still reword at `session.append`.) A stricter answer of | |
| 155 | - theirs (a deny over an ask) stands. | |
| 156 | -- Any rule and any classic hook counts, whoever configured it: a verdict | |
| 157 | - carries the rule as written and the hook's event, never where either was | |
| 158 | - read from. A deny holds unnamed because it can stand in front of a rule's or | |
| 159 | - a hook's ask: lifted, the call would run with nobody asked. An ask that names | |
| 160 | - neither (the mode's own, a check of the engine's own) is not held. | |
| 150 | + chain's answer whatever decided it; an ask, when it names what decided it, | |
| 151 | + a settings rule (`rule`), a classic hook (`hook`), a setting whose own | |
| 152 | + check asked (`setting`) or an organization's ask cap on the tool | |
| 153 | + (`ceiling`), or names a host that answers it (`decider`). It is returned | |
| 154 | + whole, so the reason this event answers, which a person reads in the | |
| 155 | + prompt and the engine writes into a refusal, is the engine's, and what it | |
| 156 | + names is still named. (The refusal then enters the conversation as a tool | |
| 157 | + result, a row a person's plugin may still reword at `session.append`.) A | |
| 158 | + stricter answer of theirs (a deny over an ask) stands. | |
| 159 | +- Any rule, any classic hook, any such setting and any such cap counts, | |
| 160 | + whoever configured it: a verdict carries the rule as written, the hook's | |
| 161 | + event, the setting's key and the cap, never where any was read from. A | |
| 162 | + deny holds unnamed because | |
| 163 | + it can stand in front of such an ask: lifted, the call would run with nobody | |
| 164 | + asked. An ask that names none of them and that the person answers at a | |
| 165 | + dialog (the mode's own, a check of the engine's own that no setting turns | |
| 166 | + on) is not held. | |
| 167 | +- The setting the engine names today is the read block's | |
| 168 | + (`permissions.blockReadsOutsideWorkingDirectories`), for the two asks the | |
| 169 | + block keeps for the person: about a shell command that reads outside the | |
| 170 | + working directories, and, while the block is on, about a command the model | |
| 171 | + wants run outside the sandbox, which is what holds its reads in. The file | |
| 172 | + tools refuse such a read outright, and a deny holds as it is. | |
| 173 | +- A host: where the permission callback of the program that runs the session | |
| 174 | + answers its asks (an SDK host's `can_use_tool`, a permission prompt tool; | |
| 175 | + a cloud session's host is one), the engine's own ask says so | |
| 176 | + (`decider: "host"`), in every loop of the process, a subagent's included, | |
| 177 | + and only where the host would be asked. Such an ask holds, the mode's own | |
| 178 | + too: it goes on to whatever the mode puts between an ask and the host | |
| 179 | + (auto mode's classifier) and then to the callback, as it would with no | |
| 180 | + plugin loaded. An evaluation the engine has not classified is held. | |
| 181 | +- Where no host can be asked on an evaluation, it names none, and a | |
| 182 | + person's plugin's allow answers as it does with no host. That is a closed | |
| 183 | + list, pinned by a test: an in-process teammate's own function (under a | |
| 184 | + headless leader its ask is cancelled or never answered); the comment | |
| 185 | + auto-edit's own checks (one that is not an allow ends in a notice); the | |
| 186 | + observer's gate (an ask there is a deny); a fork, subtask, workflow or | |
| 187 | + forked command that has no session function of its own; and a skill's | |
| 188 | + inline shell command where no hand-off exists, since an ask then fails | |
| 189 | + the render. With a hand-off, in auto mode, that command's ask is left to | |
| 190 | + the model's own call, which the host is asked about: it holds. | |
| 191 | +- An ask that is certain to be refused with nobody asked names no host, so | |
| 192 | + a person's plugin's allow answers it as it does in a session no host | |
| 193 | + runs: under `dontAsk`, in a loop no prompt can show in (a background | |
| 194 | + agent's; in auto mode there, with no chain, the plugin's allow comes | |
| 195 | + before the classifier, as it does on main), once a permission prompt | |
| 196 | + tool's server has left the session. An ask only a dialog on this computer | |
| 197 | + can show holds under a prompt tool and on a served call: the engine denies | |
| 198 | + it there, and a plugin's allow does not turn that deny into an allow. | |
| 199 | + What is not certain holds: every other mode, a | |
| 200 | + Chrome tool's ask under `dontAsk` (a site grant sends it on), and, where | |
| 201 | + the host is set to be asked again after every allow auto mode's | |
| 202 | + classifier gives, a loop no prompt can show in. | |
| 203 | +- A call this computer runs for a cloud session names that session as its | |
| 204 | + host: its ask goes there and holds. Serving marks nothing for this | |
| 205 | + computer's own session: its calls name a host only if one answers them. | |
| 206 | +- The two hosts the engine knows to show the question to the person on a | |
| 207 | + card (the IDE pane, the desktop app's Code tab) are that person's dialog | |
| 208 | + and are not named. The session's start says which it is, once, for the | |
| 209 | + hold and for the card alike; nothing written later changes it. Only the | |
| 210 | + host's own request is such a card: a permission prompt tool's question | |
| 211 | + holds in a pane too. The | |
| 212 | + entrypoint is asserted by the start environment, not proven: whoever sets | |
| 213 | + the environment a session is spawned with can claim a pane. | |
| 161 | 214 | - An organization's plugin (prepend or append) or a built-in that allows |
| 162 | 215 | over a deny rule takes part in both runs, so its answer stands (a prepended |
| 163 | 216 | one that loosens is what brings the second run about, so its hooks run |
| 164 | 217 | twice on such a call). The mode's own ask |
| 165 | - that a person's plugin turns into an allow, with no rule or hook behind it, | |
| 166 | - stands: that is what such a plugin is for. | |
| 218 | + that a person's plugin turns into an allow, with nothing named behind it | |
| 219 | + and the person the one who would have been asked, stands: that is what such | |
| 220 | + a plugin is for. | |
| 167 | 221 | - `tool.check` pins the question (`tool`, `input`, `tool_use_id`), so no hook |
| 168 | 222 | can have the rules evaluated on one command and another run; a rewrite |
| 169 | 223 | belongs to `tool.call`, which runs before any of this. |
| @@ -172,8 +226,9 @@ | ||
| 172 | 226 | settings from a <tool> call (<rule>); the deny rule holds over the plugins |
| 173 | 227 | you install (allowModsToOverrideDenyRules)`, or `tried to reword` where |
| 174 | 228 | the decision was kept and its words or its rule were not. The kinds are `a deny rule`, |
| 175 | - `an ask rule`, `a <event> hook's ask` and `a refusal` (a deny that names no | |
| 176 | - rule); the last two carry neither `in your settings` nor a rule. Plugins the | |
| 229 | + `an ask rule`, `a <event> hook's ask`, `a <key> setting's ask`, `a host's | |
| 230 | + ask` and `a refusal` (a deny that names no rule); only the two rule kinds | |
| 231 | + carry `in your settings` and a rule. Plugins the | |
| 177 | 232 | engine ran as one |
| 178 | 233 | batch are named together, as it names them (`audit+easy`). A plain `-p` |
| 179 | 234 | run has it in the debug log alone; the call is still denied, or asked |
| @@ -184,10 +239,18 @@ | ||
| 184 | 239 | - What this does not reach: a held ask is still an ask, so whatever answers |
| 185 | 240 | asks for the person answers it, a classic `PermissionRequest` hook of |
| 186 | 241 | theirs included (`allowManagedHooksOnly` is the control for those). Nor an |
| 187 | - ask that names neither a rule nor a hook though the engine's own check | |
| 188 | - raised it (the read block's ask on a shell command, a dangerous removal's): | |
| 189 | - the verdict does not tell it from the mode's own, so a person's plugin may | |
| 190 | - still answer it. | |
| 242 | + ask of the engine's own check that no setting turns on (a dangerous | |
| 243 | + removal's): at a dialog the verdict does not tell it from the mode's own, | |
| 244 | + so a person's plugin may still answer it there. Nor a session nobody | |
| 245 | + answers asks for (a plain `-p` run, where an ask is refused): the mode's | |
| 246 | + own ask is a person's plugin's to allow there. Nor a hosted session where | |
| 247 | + this plugin is not seated: a person's plugin answers the mode's ask there | |
| 248 | + as it does at a dialog. Nor an ask an organization's own plugin answers | |
| 249 | + without passing on the engine's verdict: it names nothing unless that | |
| 250 | + plugin names it. | |
| 251 | +- It does not reach a person's classic `PreToolUse` hook that allows | |
| 252 | + either: the engine then raises no ask at all, `tool.check` never runs, and | |
| 253 | + the host is not asked. That is the same with no plugin loaded. | |
| 191 | 254 | - If the hook itself fails, its `.catch` answers from the one run it can |
| 192 | 255 | read: a verdict no plugin of the person's rewrote stands, one they |
| 193 | 256 | tightened included; one they rewrote, or a run that rejected, is refused |
| @@ -228,6 +291,20 @@ | ||
| 228 | 291 | organization's own plugin's reads, writes and log lines continue past the |
| 229 | 292 | user tier's hooks (the `env.get`, `env.set` and `ui.log` rows). |
| 230 | 293 | |
| 294 | +It refuses the same of ten variables that can load code into a child the | |
| 295 | +engine starts, a managed hook's command among them: the loader's | |
| 296 | +(`LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, | |
| 297 | +`DYLD_LIBRARY_PATH`, `DYLD_FRAMEWORK_PATH`), a shell's start file | |
| 298 | +(`BASH_ENV`, `ENV`, `ZDOTDIR`) and the runtime's options (`NODE_OPTIONS`). | |
| 299 | +Each reaches some children, not all: `BASH_ENV` a bash that is not | |
| 300 | +interactive, `ENV` an interactive shell in sh mode, `NODE_OPTIONS` a Node | |
| 301 | +child, and macOS drops the `DYLD_` names for protected binaries. | |
| 302 | +That pin is a list, not a boundary. A person's plugin can start a process | |
| 303 | +as the person by other roads, and other variables steer a child too | |
| 304 | +(`PATH`, the home variables, a credential file's path). The pin keeps the | |
| 305 | +organization's hook commands and tools from being re-pointed by accident | |
| 306 | +or by a careless plugin, not by a hostile one. | |
| 307 | + | |
| 231 | 308 | - At a load, from the first admission until the set serves, an op a plugin |
| 232 | 309 | makes (one it left running between the load's steps too) is raised through |
| 233 | 310 | the plugins admitted so far, this one first: a pinned variable is |
| @@ -1,0 +1,26 @@ | ||
| 1 | +import type { EventResult } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Names who asked, where no rule decided a verdict that holds: the classic | |
| 5 | + * hook first, then the setting whose check asked, then the host it goes to. | |
| 6 | + * | |
| 7 | + * An ask that names none of the three holds by the ceiling an organization | |
| 8 | + * set on the tool. | |
| 9 | + * | |
| 10 | + * @param held the ask in question | |
| 11 | + * @returns `<event> hook's ask`, `<key> setting's ask`, `host's ask`, or | |
| 12 | + * `ask cap` | |
| 13 | + */ | |
| 14 | +export function askKind(held: EventResult<'tool.check'>) { | |
| 15 | + const isHooked = held.hook !== undefined | |
| 16 | + const isSet = held.setting !== undefined | |
| 17 | + const isHosted = held.decider !== undefined | |
| 18 | + | |
| 19 | + return isHooked | |
| 20 | + ? `${held.hook} hook's ask` | |
| 21 | + : isSet | |
| 22 | + ? `${held.setting} setting's ask` | |
| 23 | + : isHosted | |
| 24 | + ? `${held.decider}'s ask` | |
| 25 | + : 'ask cap' | |
| 26 | +} |
| @@ -1,11 +1,13 @@ | ||
| 1 | 1 | import type { EventResult } from 'claude-code' |
| 2 | 2 | |
| 3 | +import { askKind } from './ask-kind.js' | |
| 4 | + | |
| 3 | 5 | /** |
| 4 | 6 | * What kind of thing held over a person's plugin, as the notice calls it: |
| 5 | - * `deny rule`, `ask rule`, `<event> hook's ask`, `ask cap`, or `refusal`. | |
| 7 | + * `deny rule`, `ask rule`, `refusal`, or whose ask it was, else `ask cap`. | |
| 6 | 8 | * |
| 7 | 9 | * A deny that names no rule is a refusal even when it names a hook: the hook |
| 8 | - * may only have asked, and something else refused. An ask that names neither | |
| 10 | + * may only have asked, and something else refused. An ask that names nobody | |
| 9 | 11 | * holds by the ceiling an organization set on the tool. |
| 10 | 12 | * |
| 11 | 13 | * @param held the verdict that holds |
| @@ -14,13 +16,10 @@ | ||
| 14 | 16 | export function heldKind(held: EventResult<'tool.check'>): string { |
| 15 | 17 | const isRuled = held.rule !== undefined |
| 16 | 18 | const isRefused = held.decision === 'deny' |
| 17 | - const isHooked = held.hook !== undefined | |
| 18 | 19 | |
| 19 | 20 | return isRuled |
| 20 | 21 | ? `${held.decision} rule` |
| 21 | 22 | : isRefused |
| 22 | 23 | ? 'refusal' |
| 23 | - : isHooked | |
| 24 | - ? `${held.hook} hook's ask` | |
| 25 | - : 'ask cap' | |
| 24 | + : askKind(held) | |
| 26 | 25 | } |
| @@ -1,3 +1,4 @@ | ||
| 1 | +export * from './ask-kind.js' | |
| 1 | 2 | export * from './caught-answer.js' |
| 2 | 3 | export * from './held-kind.js' |
| 3 | 4 | export * from './held-notice.js' |
| @@ -5,12 +5,11 @@ | ||
| 5 | 5 | |
| 6 | 6 | /** |
| 7 | 7 | * Whether the verdict reached past the user tier holds whole over the |
| 8 | - * chain's answer: a deny, or an ask a rule, a hook or a ceiling stands behind. | |
| 8 | + * chain's answer: a deny, or an ask that names what stands behind it. | |
| 9 | 9 | * |
| 10 | 10 | * It holds over a more permissive answer and over the same decision reworded |
| 11 | - * or stripped of its rule or hook; a stricter answer stands. Any rule or | |
| 12 | - * classic hook counts, whoever configured it, and so does the ceiling an | |
| 13 | - * organization set on the tool. The mode's own ask, with none, never holds. | |
| 11 | + * or stripped of what it names; a stricter answer stands. A rule, a hook, a | |
| 12 | + * setting, a host or a ceiling counts; the mode's own ask at a dialog never. | |
| 14 | 13 | * |
| 15 | 14 | * @param held what the run past the user tier settled on |
| 16 | 15 | * @param answer what the whole chain settled on |
| @@ -25,4 +24,6 @@ | ||
| 25 | 24 | (held.decision === 'deny' || |
| 26 | 25 | held.rule !== undefined || |
| 27 | 26 | held.hook !== undefined || |
| 27 | + held.setting !== undefined || | |
| 28 | + held.decider !== undefined || | |
| 28 | 29 | held.ceiling !== undefined) |
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 4 | * Whether two `tool.check` verdicts say the same in every field: decision, |
| 5 | - * reason, and the rule, hook or ceiling either names. | |
| 5 | + * reason, and whatever either names (a rule, a hook, a setting, a host). | |
| 6 | 6 | * |
| 7 | 7 | * @param one a verdict |
| 8 | 8 | * @param other another, or undefined when nothing settled |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | { |
| 2 | - "description": "Security default: from the outermost seat, continues past the user tier on the organization's classic hooks, prompt content, settings and subjects, at session.append for the rows the engine injects, a compaction's rows, a skill the Skill tool hands over and an organization plugin's notes, and at session.compact, refuses a user-tier tool.register under an MCP allowlist or under a name that reads as one of the organization's servers or plugins, restores the organization's tools in tool.list, holds a deny, and an ask a settings rule, a classic hook or an organization's ask cap decided, whole over what the user tier makes of it on tool.check, continues past the user tier's hooks for an organization plugin's log lines and its reads and writes of the environment, refuses a user-tier write of a variable managed env sets, of the launcher's variable where policy names a launcher and of the variables that move where policy and sign-in are read from, and refuses a user-tier hooks module at plugin.register while managed settings set its allowManagedModsOnly option", | |
| 2 | + "description": "Security default: from the outermost seat, continues past the user tier on the organization's classic hooks, prompt content, settings and subjects, at session.append for the rows the engine injects, a compaction's rows, a skill the Skill tool hands over and an organization plugin's notes, and at session.compact, refuses a user-tier tool.register under an MCP allowlist or under a name that reads as one of the organization's servers or plugins, restores the organization's tools in tool.list, holds a deny, an ask a settings rule, a classic hook, a setting's own check or an organization's ask cap decided, and an ask a host answers, whole over what the user tier makes of it on tool.check, continues past the user tier's hooks for an organization plugin's log lines and its reads and writes of the environment, refuses a user-tier write of a variable managed env sets, of the launcher's variable where policy names a launcher and of the variables that move where policy and sign-in are read from, and refuses a user-tier hooks module at plugin.register while managed settings set its allowManagedModsOnly option", | |
| 3 | 3 | "modules": ["./register.ts"] |
| 4 | 4 | } |
| @@ -1,0 +1,20 @@ | ||
| 1 | +/** | |
| 2 | + * Variables that can load code into a child the engine starts: at the | |
| 3 | + * loader, at a shell's start, or with the runtime's own options. | |
| 4 | + * | |
| 5 | + * A list, not a boundary. A person's plugin can start a process as the | |
| 6 | + * person by other roads; this keeps the organization's hook commands and | |
| 7 | + * tools from being re-pointed by accident or by a careless plugin. | |
| 8 | + */ | |
| 9 | +export const CODE_LOADING_VARIABLES = [ | |
| 10 | + 'LD_PRELOAD', | |
| 11 | + 'LD_LIBRARY_PATH', | |
| 12 | + 'LD_AUDIT', | |
| 13 | + 'DYLD_INSERT_LIBRARIES', | |
| 14 | + 'DYLD_LIBRARY_PATH', | |
| 15 | + 'DYLD_FRAMEWORK_PATH', | |
| 16 | + 'BASH_ENV', | |
| 17 | + 'ENV', | |
| 18 | + 'ZDOTDIR', | |
| 19 | + 'NODE_OPTIONS', | |
| 20 | +] as const |
| @@ -1,3 +1,4 @@ | ||
| 1 | +export * from './code-loading-variables.js' | |
| 1 | 2 | export * from './create-policy-memo' |
| 2 | 3 | export * from './decided-by-policy.js' |
| 3 | 4 | export * from './deny-rules-hold.js' |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { Settings } from 'claude-code' |
| 2 | 2 | |
| 3 | +import { CODE_LOADING_VARIABLES } from './code-loading-variables.js' | |
| 3 | 4 | import { POLICY_SOURCE_VARIABLES } from './policy-source-variables.js' |
| 4 | 5 | import { PROCESS_WRAPPER_VARIABLE } from './process-wrapper-variable.js' |
| 5 | 6 | |
| @@ -7,7 +8,8 @@ | ||
| 7 | 8 | * The environment variables managed policy sets: every name of its `env`, |
| 8 | 9 | * and the launcher's variable where it names one with `processWrapper`. |
| 9 | 10 | * |
| 10 | - * Always among them: the variables that say where policy is read from. | |
| 11 | + * Always among them: the variables that say where policy is read from, | |
| 12 | + * and the ones that can load code into a child the engine starts. | |
| 11 | 13 | * |
| 12 | 14 | * @param policy the managed settings, as `$.settings.read` answers them |
| 13 | 15 | * @returns the names, as the policy spells them |
| @@ -19,5 +21,6 @@ | ||
| 19 | 21 | ...Object.keys(policy.env ?? {}), |
| 20 | 22 | ...(isWrapped ? [PROCESS_WRAPPER_VARIABLE] : []), |
| 21 | 23 | ...POLICY_SOURCE_VARIABLES, |
| 24 | + ...CODE_LOADING_VARIABLES, | |
| 22 | 25 | ] |
| 23 | 26 | } |
| @@ -49,7 +49,11 @@ | ||
| 49 | 49 | export * from './seated-suite.js' |
| 50 | 50 | export * from './server-policy.js' |
| 51 | 51 | export * from './session.js' |
| 52 | +export * from './setting-bash-env.js' | |
| 53 | +export * from './setting-dyld-insert.js' | |
| 54 | +export * from './setting-ld-preload.js' | |
| 52 | 55 | export * from './setting-lower-case.js' |
| 56 | +export * from './setting-node-options.js' | |
| 53 | 57 | export * from './setting-other.js' |
| 54 | 58 | export * from './setting-proxy.js' |
| 55 | 59 | export * from './signing.js' |
| @@ -63,6 +67,7 @@ | ||
| 63 | 67 | export * from './tools-registered.js' |
| 64 | 68 | export * from './tools.js' |
| 65 | 69 | export * from './unfoldable-policy.js' |
| 70 | +export * from './unsetting-node-options.js' | |
| 66 | 71 | export * from './unsetting-proxy.js' |
| 67 | 72 | export * from './user-reachable-providers.js' |
| 68 | 73 | export * from './variables-set.js' |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { Plugin } from 'claude-code/testing' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A plugin that sets `BASH_ENV` (a shell's start file) to `mine` when the | |
| 5 | + * session starts, in the tier given: the person's own by default. | |
| 6 | + * | |
| 7 | + * It logs a refusal and goes on. | |
| 8 | + * | |
| 9 | + * @param name the plugin's name | |
| 10 | + * @param tier the tier it loads in | |
| 11 | + * @returns the plugin | |
| 12 | + */ | |
| 13 | +export const settingBashEnv = ( | |
| 14 | + name: string, | |
| 15 | + tier?: Plugin['tier'], | |
| 16 | +): Plugin => ({ | |
| 17 | + name, | |
| 18 | + tier, | |
| 19 | + register(on) { | |
| 20 | + on('session.start', async ($, e, next) => { | |
| 21 | + await $.env | |
| 22 | + .set('BASH_ENV', 'mine') | |
| 23 | + .catch((error: unknown) => $.ui.log(String(error))) | |
| 24 | + | |
| 25 | + return next(e) | |
| 26 | + }) | |
| 27 | + }, | |
| 28 | +}) |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { Plugin } from 'claude-code/testing' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A plugin that sets `DYLD_INSERT_LIBRARIES` (the macOS loader's) to `mine` | |
| 5 | + * when the session starts, in the tier given: the person's own by default. | |
| 6 | + * | |
| 7 | + * It logs a refusal and goes on. | |
| 8 | + * | |
| 9 | + * @param name the plugin's name | |
| 10 | + * @param tier the tier it loads in | |
| 11 | + * @returns the plugin | |
| 12 | + */ | |
| 13 | +export const settingDyldInsert = ( | |
| 14 | + name: string, | |
| 15 | + tier?: Plugin['tier'], | |
| 16 | +): Plugin => ({ | |
| 17 | + name, | |
| 18 | + tier, | |
| 19 | + register(on) { | |
| 20 | + on('session.start', async ($, e, next) => { | |
| 21 | + await $.env | |
| 22 | + .set('DYLD_INSERT_LIBRARIES', 'mine') | |
| 23 | + .catch((error: unknown) => $.ui.log(String(error))) | |
| 24 | + | |
| 25 | + return next(e) | |
| 26 | + }) | |
| 27 | + }, | |
| 28 | +}) |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { Plugin } from 'claude-code/testing' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A plugin that sets `LD_PRELOAD` (the Linux loader's) to `mine` when the | |
| 5 | + * session starts, in the tier given: the person's own by default. | |
| 6 | + * | |
| 7 | + * It logs a refusal and goes on. | |
| 8 | + * | |
| 9 | + * @param name the plugin's name | |
| 10 | + * @param tier the tier it loads in | |
| 11 | + * @returns the plugin | |
| 12 | + */ | |
| 13 | +export const settingLdPreload = ( | |
| 14 | + name: string, | |
| 15 | + tier?: Plugin['tier'], | |
| 16 | +): Plugin => ({ | |
| 17 | + name, | |
| 18 | + tier, | |
| 19 | + register(on) { | |
| 20 | + on('session.start', async ($, e, next) => { | |
| 21 | + await $.env | |
| 22 | + .set('LD_PRELOAD', 'mine') | |
| 23 | + .catch((error: unknown) => $.ui.log(String(error))) | |
| 24 | + | |
| 25 | + return next(e) | |
| 26 | + }) | |
| 27 | + }, | |
| 28 | +}) |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { Plugin } from 'claude-code/testing' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A plugin that sets `NODE_OPTIONS` (the runtime's options) to `mine` when the | |
| 5 | + * session starts, in the tier given: the person's own by default. | |
| 6 | + * | |
| 7 | + * It logs a refusal and goes on. | |
| 8 | + * | |
| 9 | + * @param name the plugin's name | |
| 10 | + * @param tier the tier it loads in | |
| 11 | + * @returns the plugin | |
| 12 | + */ | |
| 13 | +export const settingNodeOptions = ( | |
| 14 | + name: string, | |
| 15 | + tier?: Plugin['tier'], | |
| 16 | +): Plugin => ({ | |
| 17 | + name, | |
| 18 | + tier, | |
| 19 | + register(on) { | |
| 20 | + on('session.start', async ($, e, next) => { | |
| 21 | + await $.env | |
| 22 | + .set('NODE_OPTIONS', 'mine') | |
| 23 | + .catch((error: unknown) => $.ui.log(String(error))) | |
| 24 | + | |
| 25 | + return next(e) | |
| 26 | + }) | |
| 27 | + }, | |
| 28 | +}) |
| @@ -1,0 +1,11 @@ | ||
| 1 | +import type { EventResult } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * The engine's verdict in a hosted session when nothing allows or denies | |
| 5 | + * the call: the mode's own ask, which the host answers. | |
| 6 | + */ | |
| 7 | +export const HOST_ASK: EventResult<'tool.check'> = { | |
| 8 | + decision: 'ask', | |
| 9 | + reason: 'Bash needs approval', | |
| 10 | + decider: 'host', | |
| 11 | +} |
| @@ -14,6 +14,7 @@ | ||
| 14 | 14 | export * from './forging.js' |
| 15 | 15 | export * from './hook-ask.js' |
| 16 | 16 | export * from './hook-deny.js' |
| 17 | +export * from './host-ask.js' | |
| 17 | 18 | export * from './link-of.js' |
| 18 | 19 | export * from './listening.js' |
| 19 | 20 | export * from './muting.js' |
| @@ -22,6 +23,7 @@ | ||
| 22 | 23 | export * from './rewriting.js' |
| 23 | 24 | export * from './rule-ask.js' |
| 24 | 25 | export * from './rule-deny.js' |
| 26 | +export * from './setting-ask.js' | |
| 25 | 27 | export * from './sneaking.js' |
| 26 | 28 | export * from './tightening.js' |
| 27 | 29 |
| @@ -1,0 +1,11 @@ | ||
| 1 | +import type { EventResult } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * The engine's verdict when the read block asked about a shell read outside | |
| 5 | + * the working directories: the ask, its sentence, and the setting's key. | |
| 6 | + */ | |
| 7 | +export const SETTING_ASK: EventResult<'tool.check'> = { | |
| 8 | + decision: 'ask', | |
| 9 | + reason: 'cat names a path outside the working directories', | |
| 10 | + setting: 'permissions.blockReadsOutsideWorkingDirectories', | |
| 11 | +} |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { Plugin } from 'claude-code/testing' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A plugin that unsets `NODE_OPTIONS` when the session starts, in the tier | |
| 5 | + * given: the person's own by default. | |
| 6 | + * | |
| 7 | + * It logs a refusal and goes on. | |
| 8 | + * | |
| 9 | + * @param name the plugin's name | |
| 10 | + * @param tier the tier it loads in | |
| 11 | + * @returns the plugin | |
| 12 | + */ | |
| 13 | +export const unsettingNodeOptions = ( | |
| 14 | + name: string, | |
| 15 | + tier?: Plugin['tier'], | |
| 16 | +): Plugin => ({ | |
| 17 | + name, | |
| 18 | + tier, | |
| 19 | + register(on) { | |
| 20 | + on('session.start', async ($, e, next) => { | |
| 21 | + await $.env | |
| 22 | + .set('NODE_OPTIONS', undefined) | |
| 23 | + .catch((error: unknown) => $.ui.log(String(error))) | |
| 24 | + | |
| 25 | + return next(e) | |
| 26 | + }) | |
| 27 | + }, | |
| 28 | +}) |
| @@ -20,6 +20,51 @@ | ||
| 20 | 20 | ).toEqual([true, true, true, true, true, false, false]) |
| 21 | 21 | }) |
| 22 | 22 | |
| 23 | + test("a setting's ask and a host's hold over an allow and a rewording", () => { | |
| 24 | + expect( | |
| 25 | + [Fixtures.SETTING_ASK, Fixtures.HOST_ASK].map(held => [ | |
| 26 | + Hooks.holdsOver(held, Fixtures.ALLOWED), | |
| 27 | + Hooks.holdsOver(held, Fixtures.ASKED), | |
| 28 | + Hooks.holdsOver(held, { ...held }), | |
| 29 | + ]), | |
| 30 | + ).toEqual([ | |
| 31 | + [true, true, false], | |
| 32 | + [true, true, false], | |
| 33 | + ]) | |
| 34 | + }) | |
| 35 | + | |
| 36 | + test('a setting or decider the answer itself names holds nothing', () => { | |
| 37 | + expect( | |
| 38 | + Hooks.holdsOver(Fixtures.ASKED, { | |
| 39 | + ...Fixtures.ALLOWED, | |
| 40 | + setting: 'permissions.blockReadsOutsideWorkingDirectories', | |
| 41 | + decider: 'host', | |
| 42 | + }), | |
| 43 | + ).toBe(false) | |
| 44 | + }) | |
| 45 | + | |
| 46 | + test('an ask is named for its hook, then its setting, then its host', () => { | |
| 47 | + expect( | |
| 48 | + [ | |
| 49 | + { ...Fixtures.HOST_ASK, ...Fixtures.SETTING_ASK, ...Fixtures.HOOK_ASK }, | |
| 50 | + { ...Fixtures.HOST_ASK, ...Fixtures.SETTING_ASK }, | |
| 51 | + Fixtures.HOST_ASK, | |
| 52 | + ].map(held => Hooks.heldNotice('easy', 'Bash', held, 'lift')), | |
| 53 | + ).toEqual( | |
| 54 | + [ | |
| 55 | + "a PreToolUse hook's ask from a Bash call; the PreToolUse hook's ask", | |
| 56 | + "a permissions.blockReadsOutsideWorkingDirectories setting's ask " + | |
| 57 | + 'from a Bash call; the ' + | |
| 58 | + "permissions.blockReadsOutsideWorkingDirectories setting's ask", | |
| 59 | + "a host's ask from a Bash call; the host's ask", | |
| 60 | + ].map( | |
| 61 | + middle => | |
| 62 | + `easy tried to lift ${middle} holds over the plugins you install ` + | |
| 63 | + '(allowModsToOverrideDenyRules)', | |
| 64 | + ), | |
| 65 | + ) | |
| 66 | + }) | |
| 67 | + | |
| 23 | 68 | test('over the same decision reworded; a stricter answer stands', () => { |
| 24 | 69 | expect( |
| 25 | 70 | [ |
| @@ -811,6 +811,55 @@ | ||
| 811 | 811 | ) |
| 812 | 812 | |
| 813 | 813 | test( |
| 814 | + "the read block's ask on a shell read holds over their allow", | |
| 815 | + { plugins: [Fixtures.allowing('easy')] }, | |
| 816 | + async ($, on) => { | |
| 817 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 818 | + | |
| 819 | + const lines = Fixtures.logged(on) | |
| 820 | + | |
| 821 | + Fixtures.checksAnswered(on, Fixtures.SETTING_ASK) | |
| 822 | + | |
| 823 | + expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.SETTING_ASK) | |
| 824 | + | |
| 825 | + expect(lines).toEqual([ | |
| 826 | + `transcript: ${Hooks.heldNotice('easy', 'Bash', Fixtures.SETTING_ASK, 'lift')}`, | |
| 827 | + ]) | |
| 828 | + }, | |
| 829 | + ) | |
| 830 | + | |
| 831 | + test( | |
| 832 | + "the mode's own ask, where a host answers it, holds over their allow", | |
| 833 | + { plugins: [Fixtures.allowing('easy')] }, | |
| 834 | + async ($, on) => { | |
| 835 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 836 | + | |
| 837 | + const lines = Fixtures.logged(on) | |
| 838 | + | |
| 839 | + Fixtures.checksAnswered(on, Fixtures.HOST_ASK) | |
| 840 | + | |
| 841 | + expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.HOST_ASK) | |
| 842 | + | |
| 843 | + expect(lines).toEqual([ | |
| 844 | + `transcript: ${Hooks.heldNotice('easy', 'Bash', Fixtures.HOST_ASK, 'lift')}`, | |
| 845 | + ]) | |
| 846 | + }, | |
| 847 | + ) | |
| 848 | + | |
| 849 | + test( | |
| 850 | + "an organization plugin's allow over a host's ask stands", | |
| 851 | + { plugins: [Fixtures.allowing('corp', 'append')] }, | |
| 852 | + async ($, on) => { | |
| 853 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 854 | + Fixtures.checksAnswered(on, Fixtures.HOST_ASK) | |
| 855 | + | |
| 856 | + expect(await $.tool.check(Fixtures.CHECKED)).toEqual({ | |
| 857 | + decision: 'allow', | |
| 858 | + }) | |
| 859 | + }, | |
| 860 | + ) | |
| 861 | + | |
| 862 | + test( | |
| 814 | 863 | "a classic hook's deny holds over their allow", |
| 815 | 864 | { plugins: [Fixtures.allowing('easy')] }, |
| 816 | 865 | async ($, on) => { |
| @@ -1840,6 +1889,15 @@ | ||
| 1840 | 1889 | ).toEqual([true, false, false, true]) |
| 1841 | 1890 | }) |
| 1842 | 1891 | |
| 1892 | + test("a server named corp does not claim a corp-tools server's tools", () => { | |
| 1893 | + expect( | |
| 1894 | + Hooks.isOrgTool( | |
| 1895 | + { managedMcpServers: { corp: {} } }, | |
| 1896 | + 'mcp__corp-tools__x', | |
| 1897 | + ), | |
| 1898 | + ).toBe(false) | |
| 1899 | + }) | |
| 1900 | + | |
| 1843 | 1901 | test( |
| 1844 | 1902 | 'a compaction is not theirs to answer', |
| 1845 | 1903 | { plugins: [Fixtures.compacting] }, |
| @@ -2187,7 +2245,169 @@ | ||
| 2187 | 2245 | ).toEqual([true, true, true, true, true, true, true, true, true]) |
| 2188 | 2246 | }) |
| 2189 | 2247 | |
| 2248 | + test('every variable that can load code into a child is pinned', () => { | |
| 2249 | + expect( | |
| 2250 | + [ | |
| 2251 | + 'LD_PRELOAD', | |
| 2252 | + 'LD_LIBRARY_PATH', | |
| 2253 | + 'LD_AUDIT', | |
| 2254 | + 'DYLD_INSERT_LIBRARIES', | |
| 2255 | + 'DYLD_LIBRARY_PATH', | |
| 2256 | + 'DYLD_FRAMEWORK_PATH', | |
| 2257 | + 'BASH_ENV', | |
| 2258 | + 'ENV', | |
| 2259 | + 'ZDOTDIR', | |
| 2260 | + 'NODE_OPTIONS', | |
| 2261 | + 'ld_preload', | |
| 2262 | + 'PATH', | |
| 2263 | + 'HOME', | |
| 2264 | + ].map(name => Hooks.isPinnedVariable({}, name)), | |
| 2265 | + ).toEqual([ | |
| 2266 | + true, | |
| 2267 | + true, | |
| 2268 | + true, | |
| 2269 | + true, | |
| 2270 | + true, | |
| 2271 | + true, | |
| 2272 | + true, | |
| 2273 | + true, | |
| 2274 | + true, | |
| 2275 | + true, | |
| 2276 | + true, | |
| 2277 | + false, | |
| 2278 | + false, | |
| 2279 | + ]) | |
| 2280 | + }) | |
| 2281 | + | |
| 2190 | 2282 | test( |
| 2283 | + "LD_PRELOAD is not a person's plugin's to set", | |
| 2284 | + { | |
| 2285 | + plugins: [ | |
| 2286 | + Fixtures.settingLdPreload('suite', 'prepend'), | |
| 2287 | + Fixtures.settingLdPreload('mine'), | |
| 2288 | + Fixtures.settingLdPreload('bundled', 'builtin'), | |
| 2289 | + ], | |
| 2290 | + }, | |
| 2291 | + async ($, on) => { | |
| 2292 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 2293 | + | |
| 2294 | + const lines = Fixtures.logged(on) | |
| 2295 | + const set = Fixtures.variablesSet(on) | |
| 2296 | + | |
| 2297 | + await $.session.start(Fixtures.SESSION) | |
| 2298 | + | |
| 2299 | + expect(set).toEqual([ | |
| 2300 | + 'suite LD_PRELOAD=mine', | |
| 2301 | + 'bundled LD_PRELOAD=mine', | |
| 2302 | + ]) | |
| 2303 | + expect(lines).toHaveLength(1) | |
| 2304 | + expect(lines[0]).toContain(Hooks.pinnedVariableRefusal('LD_PRELOAD')) | |
| 2305 | + }, | |
| 2306 | + ) | |
| 2307 | + | |
| 2308 | + test( | |
| 2309 | + "DYLD_INSERT_LIBRARIES is not a person's plugin's to set", | |
| 2310 | + { | |
| 2311 | + plugins: [ | |
| 2312 | + Fixtures.settingDyldInsert('suite', 'prepend'), | |
| 2313 | + Fixtures.settingDyldInsert('mine'), | |
| 2314 | + Fixtures.settingDyldInsert('bundled', 'builtin'), | |
| 2315 | + ], | |
| 2316 | + }, | |
| 2317 | + async ($, on) => { | |
| 2318 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 2319 | + | |
| 2320 | + const lines = Fixtures.logged(on) | |
| 2321 | + const set = Fixtures.variablesSet(on) | |
| 2322 | + | |
| 2323 | + await $.session.start(Fixtures.SESSION) | |
| 2324 | + | |
| 2325 | + expect(set).toEqual([ | |
| 2326 | + 'suite DYLD_INSERT_LIBRARIES=mine', | |
| 2327 | + 'bundled DYLD_INSERT_LIBRARIES=mine', | |
| 2328 | + ]) | |
| 2329 | + expect(lines).toHaveLength(1) | |
| 2330 | + expect(lines[0]).toContain( | |
| 2331 | + Hooks.pinnedVariableRefusal('DYLD_INSERT_LIBRARIES'), | |
| 2332 | + ) | |
| 2333 | + }, | |
| 2334 | + ) | |
| 2335 | + | |
| 2336 | + test( | |
| 2337 | + "BASH_ENV is not a person's plugin's to set", | |
| 2338 | + { | |
| 2339 | + plugins: [ | |
| 2340 | + Fixtures.settingBashEnv('suite', 'prepend'), | |
| 2341 | + Fixtures.settingBashEnv('mine'), | |
| 2342 | + Fixtures.settingBashEnv('bundled', 'builtin'), | |
| 2343 | + ], | |
| 2344 | + }, | |
| 2345 | + async ($, on) => { | |
| 2346 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 2347 | + | |
| 2348 | + const lines = Fixtures.logged(on) | |
| 2349 | + const set = Fixtures.variablesSet(on) | |
| 2350 | + | |
| 2351 | + await $.session.start(Fixtures.SESSION) | |
| 2352 | + | |
| 2353 | + expect(set).toEqual([ | |
| 2354 | + 'suite BASH_ENV=mine', | |
| 2355 | + 'bundled BASH_ENV=mine', | |
| 2356 | + ]) | |
| 2357 | + expect(lines).toHaveLength(1) | |
| 2358 | + expect(lines[0]).toContain(Hooks.pinnedVariableRefusal('BASH_ENV')) | |
| 2359 | + }, | |
| 2360 | + ) | |
| 2361 | + | |
| 2362 | + test( | |
| 2363 | + "NODE_OPTIONS is not a person's plugin's to set", | |
| 2364 | + { | |
| 2365 | + plugins: [ | |
| 2366 | + Fixtures.settingNodeOptions('suite', 'prepend'), | |
| 2367 | + Fixtures.settingNodeOptions('mine'), | |
| 2368 | + Fixtures.settingNodeOptions('bundled', 'builtin'), | |
| 2369 | + ], | |
| 2370 | + }, | |
| 2371 | + async ($, on) => { | |
| 2372 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 2373 | + | |
| 2374 | + const lines = Fixtures.logged(on) | |
| 2375 | + const set = Fixtures.variablesSet(on) | |
| 2376 | + | |
| 2377 | + await $.session.start(Fixtures.SESSION) | |
| 2378 | + | |
| 2379 | + expect(set).toEqual([ | |
| 2380 | + 'suite NODE_OPTIONS=mine', | |
| 2381 | + 'bundled NODE_OPTIONS=mine', | |
| 2382 | + ]) | |
| 2383 | + expect(lines).toHaveLength(1) | |
| 2384 | + expect(lines[0]).toContain(Hooks.pinnedVariableRefusal('NODE_OPTIONS')) | |
| 2385 | + }, | |
| 2386 | + ) | |
| 2387 | + | |
| 2388 | + test( | |
| 2389 | + 'nor is NODE_OPTIONS theirs to unset', | |
| 2390 | + { | |
| 2391 | + plugins: [ | |
| 2392 | + Fixtures.unsettingNodeOptions('suite', 'prepend'), | |
| 2393 | + Fixtures.unsettingNodeOptions('mine'), | |
| 2394 | + ], | |
| 2395 | + }, | |
| 2396 | + async ($, on) => { | |
| 2397 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 2398 | + | |
| 2399 | + const lines = Fixtures.logged(on) | |
| 2400 | + const set = Fixtures.variablesSet(on) | |
| 2401 | + | |
| 2402 | + await $.session.start(Fixtures.SESSION) | |
| 2403 | + | |
| 2404 | + expect(set).toEqual(['suite NODE_OPTIONS=undefined']) | |
| 2405 | + expect(lines).toHaveLength(1) | |
| 2406 | + expect(lines[0]).toContain(Hooks.pinnedVariableRefusal('NODE_OPTIONS')) | |
| 2407 | + }, | |
| 2408 | + ) | |
| 2409 | + | |
| 2410 | + test( | |
| 2191 | 2411 | 'a listed id that is one at-sign refuses every registration of theirs', |
| 2192 | 2412 | { |
| 2193 | 2413 | plugins: [Fixtures.registering('mine'), Fixtures.registering('other')], |
Large file, diff not shown.