spacehub

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

sec-default: the tool.check test plugins carry their verdicts in their own bodiesBrowse files
poteat authored 24 days ago parent a92b0c5 · commit 9dfc5323aabb
9 changed files+107 -81compared with a92b0c5
Amods/sec-default/tests/fixtures/tool-check/allowing.ts
Dmods/sec-default/tests/fixtures/tool-check/answering.ts
Amods/sec-default/tests/fixtures/tool-check/asking.ts
Amods/sec-default/tests/fixtures/tool-check/blind-allowing.ts
Dmods/sec-default/tests/fixtures/tool-check/blind-answering.ts
Amods/sec-default/tests/fixtures/tool-check/forging.ts
Mmods/sec-default/tests/fixtures/tool-check/index.ts
Amods/sec-default/tests/fixtures/tool-check/tightening.ts
Mmods/sec-default/tests/register.test.ts
@@ -1,0 +1,21 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin that hears the verdict beneath it on `tool.check`, then allows
5+ * the call whatever it heard: 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
10+ */
11+export const allowing = (name: string, tier?: Plugin['tier']): Plugin => ({
12+ name,
13+ tier,
14+ register(on) {
15+ on('tool.check', async ($, e, next) => {
16+ await next(e)
17+
18+ return { decision: 'allow' }
19+ })
20+ },
21+})
Dmods/sec-default/tests/fixtures/tool-check/answering.ts+0 -27
@@ -1,27 +1,0 @@
1-import type { EventResult } from 'claude-code'
2-import type { Plugin } from 'claude-code/testing'
3-
4-/**
5- * A plugin that hears the verdict beneath it on `tool.check`, then answers
6- * its own whatever it heard: the person's own by default.
7- *
8- * @param name the plugin's name
9- * @param verdict what it answers
10- * @param tier the tier it loads in
11- * @returns the plugin
12- */
13-export const answering = (
14- name: string,
15- verdict: EventResult<'tool.check'>,
16- tier?: Plugin['tier'],
17-): Plugin => ({
18- name,
19- tier,
20- register(on) {
21- on('tool.check', async ($, e, next) => {
22- await next(e)
23-
24- return verdict
25- })
26- },
27-})
@@ -1,0 +1,19 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that hears the verdict beneath it on
5+ * `tool.check`, then puts the call to the person whatever it heard.
6+ *
7+ * @param name the plugin's name
8+ * @returns the plugin
9+ */
10+export const asking = (name: string): Plugin => ({
11+ name,
12+ register(on) {
13+ on('tool.check', async ($, e, next) => {
14+ await next(e)
15+
16+ return { decision: 'ask', reason: 'Bash needs approval' }
17+ })
18+ },
19+})
@@ -1,0 +1,12 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that allows every `tool.check` without
5+ * calling `next`, so nothing beneath it runs.
6+ */
7+export const blindAllowing: Plugin = {
8+ name: 'blind',
9+ register(on) {
10+ on('tool.check', () => ({ decision: 'allow' }))
11+ },
12+}
Dmods/sec-default/tests/fixtures/tool-check/blind-answering.ts+0 -23
@@ -1,23 +1,0 @@
1-import type { EventResult } from 'claude-code'
2-import type { Plugin } from 'claude-code/testing'
3-
4-/**
5- * A plugin that answers `tool.check` without calling `next`, so nothing
6- * beneath it runs: the person's own by default.
7- *
8- * @param name the plugin's name
9- * @param verdict what it answers
10- * @param tier the tier it loads in
11- * @returns the plugin
12- */
13-export const blindAnswering = (
14- name: string,
15- verdict: EventResult<'tool.check'>,
16- tier?: Plugin['tier'],
17-): Plugin => ({
18- name,
19- tier,
20- register(on) {
21- on('tool.check', () => verdict)
22- },
23-})
@@ -1,0 +1,16 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that answers ask over what it heard and
5+ * writes a rule of its own into the answer, as if settings held it.
6+ */
7+export const forging: Plugin = {
8+ name: 'forging',
9+ register(on) {
10+ on('tool.check', async ($, e, next) => {
11+ await next(e)
12+
13+ return { decision: 'ask', reason: 'fine by me', rule: 'Bash(ls *)' }
14+ })
15+ },
16+}
@@ -1,13 +1,16 @@
11 export * from './allowed.js'
2-export * from './answering.js'
2+export * from './allowing.js'
33 export * from './asked.js'
4-export * from './blind-answering.js'
4+export * from './asking.js'
5+export * from './blind-allowing.js'
56 export * from './checked.js'
67 export * from './checks-answered.js'
8+export * from './forging.js'
79 export * from './link-of.js'
810 export * from './override-policy-of.js'
911 export * from './plain-deny.js'
1012 export * from './rewriting.js'
1113 export * from './rule-deny.js'
14+export * from './tightening.js'
1215
1316 export * as default from '.'
@@ -1,0 +1,16 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that hears the verdict beneath it on
5+ * `tool.check`, then refuses the call whatever it heard.
6+ */
7+export const tightening: Plugin = {
8+ name: 'strict',
9+ register(on) {
10+ on('tool.check', async ($, e, next) => {
11+ await next(e)
12+
13+ return { decision: 'deny', reason: 'a PreToolUse hook refused it' }
14+ })
15+ },
16+}
@@ -309,7 +309,7 @@
309309
310310 test(
311311 'a deny rule holds over an allow from a plugin the person installed',
312- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
312+ { plugins: [Fixtures.allowing('easy')] },
313313 async ($, on) => {
314314 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
315315
@@ -328,10 +328,7 @@
328328 test(
329329 'two plugins of the person chained are each named once in a session',
330330 {
331- plugins: [
332- Fixtures.answering('first', Fixtures.ALLOWED),
333- Fixtures.answering('second', Fixtures.ASKED),
334- ],
331+ plugins: [Fixtures.allowing('first'), Fixtures.asking('second')],
335332 },
336333 async ($, on) => {
337334 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
@@ -343,7 +340,7 @@
343340 expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.RULE_DENY)
344341 expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.RULE_DENY)
345342
346- expect(lines).toEqual([
343+ expect(lines.toSorted()).toEqual([
347344 `transcript: ${Hooks.heldNotice('first', 'Bash', 'Bash(echo *)')}`,
348345 `transcript: ${Hooks.heldNotice('second', 'Bash', 'Bash(echo *)')}`,
349346 ])
@@ -352,7 +349,7 @@
352349
353350 test(
354351 'a deny rule turned into an ask by a plugin of the person holds too',
355- { plugins: [Fixtures.answering('easy', Fixtures.ASKED)] },
352+ { plugins: [Fixtures.asking('easy')] },
356353 async ($, on) => {
357354 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
358355 Fixtures.logged(on)
@@ -366,8 +363,8 @@
366363 "an organization plugin's allow over a deny rule stands, above or below",
367364 {
368365 plugins: [
369- Fixtures.answering('suite', Fixtures.ALLOWED, 'append'),
370- Fixtures.answering('easy', Fixtures.ALLOWED),
366+ Fixtures.allowing('suite', 'append'),
367+ Fixtures.allowing('easy'),
371368 ],
372369 },
373370 async ($, on) => {
@@ -384,7 +381,7 @@
384381
385382 test(
386383 "a prepended organization plugin's allow over a deny rule stands",
387- { plugins: [Fixtures.answering('guard', Fixtures.ALLOWED, 'prepend')] },
384+ { plugins: [Fixtures.allowing('guard', 'prepend')] },
388385 async ($, on) => {
389386 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
390387
@@ -399,7 +396,7 @@
399396
400397 test(
401398 "a built-in's allow over a deny rule stands",
402- { plugins: [Fixtures.answering('bundled', Fixtures.ALLOWED, 'builtin')] },
399+ { plugins: [Fixtures.allowing('bundled', 'builtin')] },
403400 async ($, on) => {
404401 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
405402
@@ -414,7 +411,7 @@
414411
415412 test(
416413 'an ask a plugin of the person allows, no deny rule behind it, stands',
417- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
414+ { plugins: [Fixtures.allowing('easy')] },
418415 async ($, on) => {
419416 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
420417
@@ -429,7 +426,7 @@
429426
430427 test(
431428 'a deny no rule decided is still theirs to answer over',
432- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
429+ { plugins: [Fixtures.allowing('easy')] },
433430 async ($, on) => {
434431 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
435432
@@ -444,7 +441,7 @@
444441
445442 test(
446443 'a plugin of the person that tightens is heard, with one evaluation',
447- { plugins: [Fixtures.answering('strict', Fixtures.PLAIN_DENY)] },
444+ { plugins: [Fixtures.tightening] },
448445 async ($, on) => {
449446 const reads = Fixtures.policyReads(on, Fixtures.MANAGED_POLICY)
450447 const evaluations = Fixtures.checksAnswered(on, Fixtures.ASKED)
@@ -472,7 +469,7 @@
472469
473470 test(
474471 'an allow that never called next meets the deny rule all the same',
475- { plugins: [Fixtures.blindAnswering('blind', Fixtures.ALLOWED)] },
472+ { plugins: [Fixtures.blindAllowing] },
476473 async ($, on) => {
477474 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
478475
@@ -492,15 +489,7 @@
492489
493490 test(
494491 'a rule a plugin of the person writes into its answer is never read',
495- {
496- plugins: [
497- Fixtures.answering('forging', {
498- decision: 'ask',
499- reason: 'fine by me',
500- rule: 'Bash(ls *)',
501- }),
502- ],
503- },
492+ { plugins: [Fixtures.forging] },
504493 async ($, on) => {
505494 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
506495
@@ -542,7 +531,7 @@
542531
543532 test(
544533 'managed settings may let the plugins a person installs override',
545- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
534+ { plugins: [Fixtures.allowing('easy')] },
546535 async ($, on) => {
547536 on('settings.read', () => ({ value: Fixtures.overridePolicyOf(true) }))
548537
@@ -560,7 +549,7 @@
560549
561550 test(
562551 "only managed settings are asked: a person's own cannot lift a deny rule",
563- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
552+ { plugins: [Fixtures.allowing('easy')] },
564553 async ($, on) => {
565554 Fixtures.policyBySource(
566555 on,
@@ -577,7 +566,7 @@
577566
578567 test(
579568 'the option mistyped lifts nothing: only the literal true does',
580- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
569+ { plugins: [Fixtures.allowing('easy')] },
581570 async ($, on) => {
582571 on('settings.read', () => ({ value: Fixtures.overridePolicyOf('true') }))
583572 Fixtures.logged(on)
@@ -589,7 +578,7 @@
589578
590579 test(
591580 'with a policy that cannot be read the deny rule holds: fails closed',
592- { plugins: [Fixtures.answering('easy', Fixtures.ALLOWED)] },
581+ { plugins: [Fixtures.allowing('easy')] },
593582 async ($, on) => {
594583 on('settings.read', () => ({ deny: 'managed settings unreadable' }))
595584 Fixtures.logged(on)
@@ -601,7 +590,7 @@
601590
602591 test(
603592 "when the rules cannot be evaluated the call is refused: the hook's catch",
604- { plugins: [Fixtures.blindAnswering('blind', Fixtures.ALLOWED)] },
593+ { plugins: [Fixtures.blindAllowing] },
605594 async ($, on) => {
606595 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
607596 Fixtures.logged(on)