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 system prompt's sections continue past the user tier (#97241)Browse files
* sec-default: the system prompt's sections continue past the user tier; the declarations carry prompt.compose

* sec-default: prompt.compose has its own row, a second case where a person's plugin asks first, and the declarations as the event shipped

* sec-default: the two new cases set their several-line hooks apart
Alice T'Poteat authored 24 days ago · GitHub committed 24 days ago parent ec44ca9 · commit 684800b20682
9 changed files+117 -1compared with ec44ca9
Mmods/sec-default/README.md
Mmods/sec-default/hooks/register.ts
Amods/sec-default/tests/fixtures/composed.ts
Amods/sec-default/tests/fixtures/emptying.ts
Amods/sec-default/tests/fixtures/heading.ts
Mmods/sec-default/tests/fixtures/index.ts
Amods/sec-default/tests/fixtures/rewording.ts
Mmods/sec-default/tests/register.test.ts
Mmods/types/claude-code.d.ts
@@ -27,6 +27,7 @@
2727 | --- | --- |
2828 | `classic.*` | Continue past the user tier: the organization's settings hooks see the engine's input and their answer stands. |
2929 | `prompt.section`, `prompt.context`, `skill.prompt`, `attribution.text` | Continue past the user tier: managed CLAUDE.md, rules and policy skills reach the model as written. A person's plugins keep `prompt.submit` and its additive context. |
30+| `prompt.compose` | Continue past the user tier: the system prompt's list of sections is what the organization's tiers, the built-ins and the engine's own composition make it. A person's plugin neither drops, reorders nor rewrites a section, nor changes the facts the list is composed from, nor answers a list of its own in its place. The engine raises this event only when some loaded plugin hooks it, so where this plugin is seated every render of the system prompt runs the chain. |
3031 | `settings.read` | Continue past the user tier: no user hook rewrites what any caller reads as settings, this plugin's own policy reads included. |
3132 | `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. |
3233 | `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); otherwise it passes. |
@@ -91,7 +92,7 @@
9192
9293 ## What it hooks
9394
94-`classic.*`, `prompt.section`, `prompt.context`, `skill.prompt`,
95+`classic.*`, `prompt.section`, `prompt.context`, `prompt.compose`, `skill.prompt`,
9596 `attribution.text`, `settings.read`, `tool.describe`, `command.describe`,
9697 `agent.offer`, `agent.spawn`, `tool.register`, `tool.list`, `tool.check`,
9798 `plugin.register`.
@@ -25,6 +25,7 @@
2525
2626 on('prompt.section', ($, e, next) => next.to(e, 'append'))
2727 on('prompt.context', ($, e, next) => next.to(e, 'append'))
28+ on('prompt.compose', ($, e, next) => next.to(e, 'append'))
2829 on('skill.prompt', ($, e, next) => next.to(e, 'append'))
2930 on('attribution.text', ($, e, next) => next.to(e, 'append'))
3031
@@ -1,0 +1,13 @@
1+import type { PromptComposeInput } from 'claude-code'
2+
3+/**
4+ * The facts of one render of the system prompt, as the engine raises them.
5+ */
6+export const COMPOSED: PromptComposeInput = {
7+ model: 'example-model-1',
8+ promptModel: 'example-model-1',
9+ surfaces: ['terminal'],
10+ tools: ['Bash'],
11+ outputStyle: null,
12+ traits: [],
13+}
@@ -1,0 +1,14 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that answers a system prompt of its own
5+ * and asks nothing of what is beneath it.
6+ */
7+export const emptying: Plugin = {
8+ name: 'emptying',
9+ register(on) {
10+ on('prompt.compose', () => ({
11+ sections: [{ id: 'emptying:all', text: 'mine alone', scope: 'session' }],
12+ }))
13+ },
14+}
@@ -1,0 +1,22 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * The organization's own plugin, in its last tier, which puts its section
5+ * at the head of the list beneath it.
6+ */
7+export const heading: Plugin = {
8+ name: 'heading',
9+ tier: 'append',
10+ register(on) {
11+ on('prompt.compose', async ($, e, next) => {
12+ const { sections } = await next(e)
13+
14+ return {
15+ sections: [
16+ { id: 'heading:org', text: 'the org says hi', scope: 'shared' },
17+ ...sections,
18+ ],
19+ }
20+ })
21+ },
22+}
@@ -2,9 +2,12 @@
22 export * from './agent-spawned.js'
33 export * from './allowlist.js'
44 export * from './command-described.js'
5+export * from './composed.js'
56 export * from './denying.js'
67 export * from './dropping.js'
8+export * from './emptying.js'
79 export * from './fullscreen.js'
10+export * from './heading.js'
811 export * from './listing.js'
912 export * from './logged.js'
1013 export * from './managed-mods-only.js'
@@ -23,6 +26,7 @@
2326 export * from './registered-tool-of.js'
2427 export * from './registering.js'
2528 export * from './relabeling.js'
29+export * from './rewording.js'
2630 export * from './server-policy.js'
2731 export * from './session.js'
2832 export * from './signing.js'
@@ -1,0 +1,23 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that asks for what is beneath it, then
5+ * drops the body and rewrites the text of every section it keeps.
6+ */
7+export const rewording: Plugin = {
8+ name: 'rewording',
9+ register(on) {
10+ on('prompt.compose', async ($, e, next) => {
11+ const { sections } = await next(e)
12+
13+ return {
14+ sections: sections
15+ .filter(section => section.id !== 'body')
16+ .map(section => ({
17+ ...section,
18+ text: `${section.text} (reworded)`,
19+ })),
20+ }
21+ })
22+ },
23+}
@@ -156,6 +156,44 @@
156156 )
157157
158158 test(
159+ "the system prompt's sections pass over the plugins the person installed",
160+ { plugins: [Fixtures.emptying, Fixtures.heading] },
161+ async ($, on) => {
162+ on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST }))
163+
164+ on('prompt.compose', () => ({
165+ sections: [{ id: 'body', text: 'the body', scope: 'shared' }],
166+ }))
167+
168+ expect(await $.prompt.compose(Fixtures.COMPOSED)).toEqual({
169+ sections: [
170+ { id: 'heading:org', text: 'the org says hi', scope: 'shared' },
171+ { id: 'body', text: 'the body', scope: 'shared' },
172+ ],
173+ })
174+ },
175+ )
176+
177+ test(
178+ "nor does a person's plugin drop or reword a section it asked for",
179+ { plugins: [Fixtures.rewording, Fixtures.heading] },
180+ async ($, on) => {
181+ on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST }))
182+
183+ on('prompt.compose', () => ({
184+ sections: [{ id: 'body', text: 'the body', scope: 'shared' }],
185+ }))
186+
187+ expect(await $.prompt.compose(Fixtures.COMPOSED)).toEqual({
188+ sections: [
189+ { id: 'heading:org', text: 'the org says hi', scope: 'shared' },
190+ { id: 'body', text: 'the body', scope: 'shared' },
191+ ],
192+ })
193+ },
194+ )
195+
196+ test(
159197 "a user plugin's rewrite of policy is skipped for every other reader",
160198 {
161199 plugins: [
Large file, diff not shown.