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
| @@ -58,11 +58,15 @@ | ||
| 58 | 58 | seated it does not: |
| 59 | 59 | |
| 60 | 60 | - The hook first runs the chain as it is. If the answer is a deny, or no |
| 61 | - user-tier link answered more permissively than the verdict handed up to it | |
| 62 | - (read off `next.trace`, whose tiers the engine pins; a link that never | |
| 63 | - called `next` is measured against a deny), the answer passes: nothing of | |
| 64 | - the person's loosened anything, and a plugin that only listens costs | |
| 65 | - nothing more. | |
| 61 | + link that may hold a person's plugin answered more permissively than the | |
| 62 | + verdict handed up to it, the answer passes: nothing of the person's | |
| 63 | + loosened anything, and a plugin that only listens costs nothing more. This | |
| 64 | + is read off `next.trace`, whose tiers the engine pins: a link that never | |
| 65 | + called `next` is measured against a deny, and since the engine lists | |
| 66 | + neighbouring plugins that share a worker as one batch under its first | |
| 67 | + member's tier, a `prepend` entry beneath this plugin counts as well as a | |
| 68 | + `user` one. Whether a person's plugin did the loosening is never settled | |
| 69 | + here, only by the next step. | |
| 66 | 70 | - Otherwise it runs the dispatch once more with the user tier left out |
| 67 | 71 | (`next.to(e, "append")`). That verdict never passed through a person's |
| 68 | 72 | plugin, so neither the decision nor the rule it names can have been |
| @@ -81,10 +85,12 @@ | ||
| 81 | 85 | can have the rules evaluated on one command and another run; a rewrite |
| 82 | 86 | belongs to `tool.call`, which runs before any of this. |
| 83 | 87 | - The person is told once for each plugin in a session, in the transcript |
| 84 | - and the debug log: `<plugin> tried to allow a <tool> call your settings | |
| 85 | - deny (<rule>); the deny rule holds over the plugins you install | |
| 86 | - (allowModsToOverrideDenyRules)`. A plain `-p` run has it in the debug log | |
| 87 | - alone; the call is still denied with the rule's own message. | |
| 88 | + and the debug log: `<plugin> tried to lift a deny rule in your settings | |
| 89 | + from a <tool> call (<rule>); the deny rule holds over the plugins you | |
| 90 | + install (allowModsToOverrideDenyRules)`. Plugins the engine ran as one | |
| 91 | + batch are named together, as it names them (`audit+easy`). A plain `-p` | |
| 92 | + run has it in the debug log alone; the call is still denied with the | |
| 93 | + rule's own message. | |
| 88 | 94 | - If the hook itself fails, its `.catch` answers from the one run it can |
| 89 | 95 | read: a deny stands; a verdict no plugin of the person's loosened stands; |
| 90 | 96 | one they loosened, or a run that rejected, is refused, since the deny |
| @@ -1,15 +1,15 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * What a person reads, once for each plugin in a session, when a plugin they |
| 3 | - * installed allowed a call that a deny rule in their settings refuses. | |
| 3 | + * installed answered allow or ask over a deny rule in their settings. | |
| 4 | 4 | * |
| 5 | 5 | * It names the option an administrator sets to let such plugins override. |
| 6 | 6 | * |
| 7 | - * @param plugin the plugin's name, as the chain's trace names its link | |
| 7 | + * @param plugin the plugin's name, or its batch's, as the trace names it | |
| 8 | 8 | * @param tool the tool the call named |
| 9 | 9 | * @param rule the deny rule that decided, as written |
| 10 | 10 | * @returns the line |
| 11 | 11 | */ |
| 12 | 12 | export const heldNotice = (plugin: string, tool: string, rule: string) => |
| 13 | - `${plugin} tried to allow a ${tool} call your settings deny (${rule}); ` + | |
| 14 | - 'the deny rule holds over the plugins you install ' + | |
| 13 | + `${plugin} tried to lift a deny rule in your settings from a ${tool} ` + | |
| 14 | + `call (${rule}); the deny rule holds over the plugins you install ` + | |
| 15 | 15 | '(allowModsToOverrideDenyRules)' |
| @@ -3,11 +3,12 @@ | ||
| 3 | 3 | import Ranking from './ranking' |
| 4 | 4 | |
| 5 | 5 | /** |
| 6 | - * The plugins a person installed that loosened the verdict in one run of | |
| 7 | - * `tool.check`: each answered more permissively than it was handed. | |
| 6 | + * The links of one run of `tool.check` that may hold a plugin a person | |
| 7 | + * installed and that answered more permissively than they were handed. | |
| 8 | 8 | * |
| 9 | - * Read off `next.trace`, whose `tier` the engine pins. A link that passed a | |
| 10 | - * verdict on, tightened it, or was skipped is not named. | |
| 9 | + * Read off `next.trace`, whose `tier` the engine pins. A batch is named as | |
| 10 | + * the engine names it, its members joined; whether a person's plugin did | |
| 11 | + * the loosening is settled by the run past the user tier, never here. | |
| 11 | 12 | * |
| 12 | 13 | * @param trace what settled beneath the hook on its latest `next` call |
| 13 | 14 | * @returns their names, nearest the caller first; none when none loosened |
| @@ -18,7 +19,7 @@ | ||
| 18 | 19 | trace |
| 19 | 20 | .filter( |
| 20 | 21 | (link, at) => |
| 21 | - link.tier === 'user' && | |
| 22 | + Ranking.TIERS_HOLDING_USERS.includes(link.tier) && | |
| 22 | 23 | Ranking.isLooser(link.returned, Ranking.handedTo(trace, at)), |
| 23 | 24 | ) |
| 24 | 25 | .map(link => link.plugin) |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | export * from './handed-to.js' |
| 2 | 2 | export * from './is-looser.js' |
| 3 | 3 | export * from './leniency' |
| 4 | +export * from './tiers-holding-users' | |
| 4 | 5 | |
| 5 | 6 | export * as default from '.' |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './tiers-holding-users.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,12 @@ | ||
| 1 | +/** | |
| 2 | + * The tiers a trace entry can carry when a plugin a person installed ran in | |
| 3 | + * it: `user`, and `prepend` beneath this plugin's own seat. | |
| 4 | + * | |
| 5 | + * The engine runs neighbouring plugins that share a worker as one batch and | |
| 6 | + * lists the batch under its first member's tier, so an organization's | |
| 7 | + * prepended plugin can stand for a batch that holds a person's. | |
| 8 | + */ | |
| 9 | +export const TIERS_HOLDING_USERS: readonly unknown[] = Object.freeze([ | |
| 10 | + 'user', | |
| 11 | + 'prepend', | |
| 12 | +]) |
| @@ -1,12 +1,17 @@ | ||
| 1 | 1 | import type { Plugin } from 'claude-code/testing' |
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | - * A plugin the person installed that hears every `tool.check` and hands up | |
| 5 | - * the verdict beneath it as it is, as one that only logs would. | |
| 4 | + * A plugin that hears every `tool.check` and hands up the verdict beneath it | |
| 5 | + * as it is, as one that only logs would: the person's own by default. | |
| 6 | + * | |
| 7 | + * @param name the plugin's name | |
| 8 | + * @param tier the tier it loads in | |
| 9 | + * @returns the plugin | |
| 6 | 10 | */ |
| 7 | -export const listening: Plugin = { | |
| 8 | - name: 'listening', | |
| 11 | +export const listening = (name: string, tier?: Plugin['tier']): Plugin => ({ | |
| 12 | + name, | |
| 13 | + tier, | |
| 9 | 14 | register(on) { |
| 10 | 15 | on('tool.check', ($, e, next) => next(e)) |
| 11 | 16 | }, |
| 12 | -} | |
| 17 | +}) |
| @@ -33,18 +33,27 @@ | ||
| 33 | 33 | ).toEqual(['blind']) |
| 34 | 34 | }) |
| 35 | 35 | |
| 36 | - test('a tightening, a skipped link and an organization link are not', () => { | |
| 36 | + test('a tightening, a skipped link and links beneath theirs are not', () => { | |
| 37 | 37 | expect( |
| 38 | 38 | Hooks.loosenedByUsers([ |
| 39 | - Fixtures.linkOf('guard', 'prepend', Fixtures.ALLOWED), | |
| 40 | 39 | Fixtures.linkOf('strict', 'user', Fixtures.PLAIN_DENY), |
| 41 | 40 | Fixtures.linkOf('passed-over', 'user'), |
| 41 | + Fixtures.linkOf('suite', 'append', Fixtures.ALLOWED), | |
| 42 | 42 | Fixtures.linkOf('bundled', 'builtin', Fixtures.ALLOWED), |
| 43 | 43 | Fixtures.linkOf('engine', 'core', Fixtures.RULE_DENY), |
| 44 | 44 | ]), |
| 45 | 45 | ).toEqual([]) |
| 46 | 46 | }) |
| 47 | 47 | |
| 48 | + test('a batch listed under prepend may hold a plugin of theirs', () => { | |
| 49 | + expect( | |
| 50 | + Hooks.loosenedByUsers([ | |
| 51 | + Fixtures.linkOf('audit+easy', 'prepend', Fixtures.ALLOWED), | |
| 52 | + Fixtures.linkOf('engine', 'core', Fixtures.RULE_DENY), | |
| 53 | + ]), | |
| 54 | + ).toEqual(['audit+easy']) | |
| 55 | + }) | |
| 56 | + | |
| 48 | 57 | test('the handler keeps a deny, and a verdict none of theirs made', () => { |
| 49 | 58 | const pastUsers = [ |
| 50 | 59 | Fixtures.linkOf('easy', 'user'), |
| @@ -456,8 +456,32 @@ | ||
| 456 | 456 | ) |
| 457 | 457 | |
| 458 | 458 | test( |
| 459 | + "a deny rule holds when an organization's plugin listens above theirs", | |
| 460 | + { | |
| 461 | + plugins: [ | |
| 462 | + Fixtures.listening('audit', 'prepend'), | |
| 463 | + Fixtures.allowing('easy'), | |
| 464 | + ], | |
| 465 | + }, | |
| 466 | + async ($, on) => { | |
| 467 | + on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) | |
| 468 | + | |
| 469 | + const lines = Fixtures.logged(on) | |
| 470 | + | |
| 471 | + Fixtures.checksAnswered(on, Fixtures.RULE_DENY) | |
| 472 | + | |
| 473 | + expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.RULE_DENY) | |
| 474 | + | |
| 475 | + expect( | |
| 476 | + lines.map(line => line.includes('easy')), | |
| 477 | + 'one line, naming the plugin of theirs, alone or in its batch', | |
| 478 | + ).toEqual([true]) | |
| 479 | + }, | |
| 480 | + ) | |
| 481 | + | |
| 482 | + test( | |
| 459 | 483 | 'a plugin of the person that only listens costs no second evaluation', |
| 460 | - { plugins: [Fixtures.listening] }, | |
| 484 | + { plugins: [Fixtures.listening('listening')] }, | |
| 461 | 485 | async ($, on) => { |
| 462 | 486 | const reads = Fixtures.policyReads(on, Fixtures.MANAGED_POLICY) |
| 463 | 487 | const evaluations = Fixtures.checksAnswered(on, Fixtures.ASKED) |