Shows the system prompt section by section in a pane, exports it to a file, and lets each section be edited through a file; /system-promt

A Claude Code mod that shows, exports and edits the system prompt of your session.
In Claude Code:
/plugin marketplace add dikeckaan/claude-system-promt
/plugin install system-promt@system-promt
It is written with Claude Code's function hooks, an early-access API: it needs a recent Claude Code (built against 2.1.288) and may break when that API changes.
| Command | What it does | |
|---|---|---|
/system-promt | Opens a pane with a section picker, the section's text, and Edit / Reset / Export / Refresh buttons | |
/system-promt list | Lists every section with its size; ✎ marks an edited one | |
/system-promt export [path] | Writes everything the model is given to a markdown file (system-prompt.md in the working directory by default) | |
/system-promt edit <section> | Copies the section to ~/.claude/system-promt/<section>.md and opens it in your text editor | |
| `/system-promt reset <section\ | all>` | Restores the original text |
/system-promt allow everything | Empties every instruction section except the protected ones; reset all restores them |
The export has four parts: the system prompt's sections, the first message's context blocks (CLAUDE.md, date, account), the messages the engine injects (skill listings, reminders), and each tool's name and description. The second and third are captured as they pass, so send one message after installing before you export. Tool parameter schemas are not included.
An edited section is sent from the next request after you save its file, and stays edited across sessions until you reset it. An empty file drops the section. Changing a section invalidates the prompt cache for the next request.
An export contains your own instructions, memory and account email. Read it before you share it.
allow everything leaves the security policy, the caution before irreversible actions and the factual sections (environment, memory, model identity) in place. It changes Claude Code's instructions only: the model's own judgment and the permission system are unchanged. Any section, protected or not, can still be edited by name.
claude plugin validate plugins/system-promt
claude plugin test plugins/system-promt
MIT, see LICENSE.
hooks/register.tsx 474 lines1import { atom, read, update } from 'claude-code'
2import type {
3 EngineInterface,
4 PromptComposeInput,
5 PromptContextBlock,
6 PromptComposeSection,
7 Register,
8 ToolInfo,
9} from 'claude-code'
10
11const NAME = 'system-promt'
12const PANE = 'system-promt'
13const OVERRIDES = 'overrides'
14const BACKUPS = 'backups'
15// Sections "allow everything" never empties: the security policy and the
16// caution before irreversible actions, and the ones that state facts (the
17// machine, the memory folder, the model). Each is still editable by name.
18const PROTECTED = [
19 'lean_body',
20 'intro',
21 'system',
22 'actions',
23 'action_caution',
24 'env_info_simple',
25 'memory',
26 'fable_identity',
27]
28const SHOWN_CHARS = 20000
29const USAGE =
30 'Usage: /system-promt [list | export [path] | edit <section> | reset <section|all> | allow everything]'
31
32const selected = atom({ plugin: 'system-promt', key: 'selected' } as const, null)
33const revision = atom({ plugin: 'system-promt', key: 'revision' } as const, 0)
34
35type Overrides = Record<string, string>
36type Backups = Record<string, string>
37type Row = PromptComposeSection & { isEdited: boolean }
38
39const compact = (n: number): string =>
40 n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
41
42const overridesOf = async ($: EngineInterface): Promise<Overrides> => {
43 const kept = await $.store.get(OVERRIDES)
44 const map: Overrides = {}
45
46 if (kept !== null && typeof kept === 'object') {
47 for (const [id, path] of Object.entries(kept)) {
48 if (typeof path === 'string') {
49 map[id] = path
50 }
51 }
52 }
53
54 return map
55}
56
57const backupsOf = async ($: EngineInterface): Promise<Backups> => {
58 const stored = await $.store.get(BACKUPS)
59 const map: Backups = {}
60
61 if (stored !== null && typeof stored === 'object') {
62 for (const [id, content] of Object.entries(stored)) {
63 if (typeof content === 'string') {
64 map[id] = content
65 }
66 }
67 }
68
69 return map
70}
71
72const saveBackup = async ($: EngineInterface, id: string, content: string): Promise<void> => {
73 const backups = await backupsOf($)
74
75 // The first, unedited text alone: a later edit never replaces it.
76 if (!(id in backups)) {
77 await $.store.set(BACKUPS, { ...backups, [id]: content })
78 }
79}
80
81const folder = async ($: EngineInterface): Promise<string> => {
82 const home = await $.env.get('HOME')
83
84 return home === undefined ? '.claude/system-promt' : `${home}/.claude/system-promt`
85}
86
87// The sections as the engine composed them, before this mod's edits; the
88// compose hook below refreshes them on every render of the prompt.
89let originals: readonly PromptComposeSection[] = []
90// The facts the engine last rendered the prompt for, so the pane composes the same one.
91let lastInput: PromptComposeInput | undefined
92// What the model reads beside the system prompt, as it passed this mod since
93// it loaded: the first message's context blocks and the engine's injected
94// messages (reminders, listings), the main conversation's alone.
95let contextBlocks: readonly PromptContextBlock[] = []
96const injected = new Map<string, string[]>()
97const files = new Map<string, { mtimeMs: number; size: number; text: string }>()
98
99// An edited section's text is its file's, read again only when the file
100// changed; undefined when the file is gone, which leaves the original.
101const edited = async (
102 $: EngineInterface,
103 path: string,
104): Promise<string | undefined> => {
105 try {
106 const stat = await $.fs.stat(path)
107 const held = files.get(path)
108
109 if (held && held.mtimeMs === stat.mtimeMs && held.size === stat.size) {
110 return held.text
111 }
112
113 const text = await $.fs.read(path)
114 files.set(path, { mtimeMs: stat.mtimeMs, size: stat.size, text })
115
116 return text
117 } catch {
118 return undefined
119 }
120}
121
122const rows = async ($: EngineInterface): Promise<Row[]> => {
123 originals = []
124 const composed = await $.prompt.compose(lastInput)
125
126 // When the call ran through this mod's own hook, `originals` is filled and
127 // `composed` already carries the edits; otherwise `composed` is the engine's.
128 const base = originals.length > 0 ? originals : composed.sections
129 originals = base
130 const overrides = await overridesOf($)
131 const list: Row[] = []
132
133 for (const one of base) {
134 const path = overrides[one.id]
135 const text = path === undefined ? undefined : await edited($, path)
136 list.push({ ...one, text: text ?? one.text, isEdited: text !== undefined })
137 }
138
139 return list
140}
141
142const exported = (
143 list: readonly Row[],
144 tools: readonly ToolInfo[],
145 when: number,
146): string => {
147 const chars = list.reduce((sum, one) => sum + one.text.length, 0)
148 const notes = [...injected].flatMap(([type, texts]) =>
149 texts.map(text => ['', `## ${type}`, '', text]).flat(),
150 )
151 const pending = '_None captured yet: send one message, then export again._'
152
153 return [
154 '# Everything the model is given',
155 '',
156 `Exported ${new Date(when).toISOString()}.`,
157 '',
158 `# 1. System prompt (${list.length} sections, ${chars} characters)`,
159 ...list.flatMap(one => [
160 '',
161 `## ${one.id} (${one.scope}${one.isEdited ? ', edited' : ''})`,
162 '',
163 one.text,
164 ]),
165 '',
166 `# 2. First-message context (${contextBlocks.length} blocks)`,
167 ...(contextBlocks.length === 0 ? ['', pending] : []),
168 ...contextBlocks.flatMap(one => ['', `## ${one.name}`, '', one.text]),
169 '',
170 `# 3. Injected messages (${notes.length / 4} captured)`,
171 ...(notes.length === 0 ? ['', pending] : notes),
172 '',
173 `# 4. Tools (${tools.length})`,
174 ...tools.flatMap(one => [
175 '',
176 `## ${one.name}${one.mcp ? ' (MCP)' : ''}`,
177 '',
178 one.description,
179 ]),
180 '',
181 ].join('\n')
182}
183
184const exportTo = async ($: EngineInterface, path: string): Promise<string> => {
185 const list = await rows($)
186 const tools = await $.tool.list()
187 await $.fs.write(path, exported(list, tools, await $.clock.now()))
188 const notes = [...injected.values()].reduce((sum, one) => sum + one.length, 0)
189
190 return `Exported ${list.length} sections, ${contextBlocks.length} context blocks, ${notes} injected messages and ${tools.length} tools to ${path}`
191}
192
193const edit = async ($: EngineInterface, id: string): Promise<string> => {
194 const list = await rows($)
195 const row = list.find(one => one.id === id)
196
197 if (!row) {
198 return `No section "${id}". Sections: ${list.map(one => one.id).join(', ')}`
199 }
200
201 const overrides = await overridesOf($)
202 let path = overrides[id]
203
204 if (path === undefined || !row.isEdited) {
205 await saveBackup($, id, row.text)
206 path = `${await folder($)}/${id.replace(/[^\w.-]/g, '_')}.md`
207 await $.fs.write(path, row.text)
208 await $.store.set(OVERRIDES, { ...overrides, [id]: path })
209 }
210
211 // Opens the file in the default text editor where the host has `open`.
212 await $.process.run(['open', '-t', path]).catch(() => undefined)
213 await update($, revision, n => n + 1)
214
215 return `Editing "${id}": ${path}\nSave the file and the next request sends its text in place of the section; an empty file drops the section. /system-promt reset ${id} restores the original.`
216}
217
218const reset = async ($: EngineInterface, id: string): Promise<string> => {
219 const overrides = await overridesOf($)
220 const backups = await backupsOf($)
221
222 const ids = id === 'all' ? Object.keys(overrides) : [id]
223 const kept = Object.fromEntries(
224 Object.entries(overrides).filter(([one]) => !ids.includes(one)),
225 )
226
227 // Restore backups to original files
228 for (const sectionId of ids) {
229 const backup = backups[sectionId]
230 const path = overrides[sectionId]
231
232 if (backup && path) {
233 await $.fs.write(path, backup)
234 }
235 }
236
237 const count = Object.keys(overrides).length - Object.keys(kept).length
238 await $.store.set(OVERRIDES, kept)
239 await $.store.set(
240 BACKUPS,
241 Object.fromEntries(Object.entries(backups).filter(([one]) => !ids.includes(one))),
242 )
243 await update($, revision, n => n + 1)
244
245 return count === 0
246 ? `Nothing to reset for "${id}".`
247 : `Restored ${count} section${count === 1 ? '' : 's'} to the original text from backups; the edited files have been updated.`
248}
249
250const allowEverything = async ($: EngineInterface): Promise<string> => {
251 const list = await rows($)
252 const overrides = await overridesOf($)
253 const dir = await folder($)
254 const removed: string[] = []
255
256 for (const section of list) {
257 if (PROTECTED.includes(section.id)) {
258 continue
259 }
260
261 if (!section.isEdited) {
262 await saveBackup($, section.id, section.text)
263 }
264
265 // An empty file drops the section from the prompt.
266 const path = `${dir}/${section.id.replace(/[^\w.-]/g, '_')}.md`
267 await $.fs.write(path, '')
268 overrides[section.id] = path
269 removed.push(section.id)
270 }
271
272 await $.store.set(OVERRIDES, overrides)
273 await update($, revision, n => n + 1)
274 const kept = list.filter(one => PROTECTED.includes(one.id)).map(one => one.id)
275
276 return [
277 `Removed ${removed.length} instruction sections from the system prompt: ${removed.join(', ') || 'none'}.`,
278 `Kept: ${kept.join(', ') || 'none'} (security policy, caution before irreversible actions, and facts about the session).`,
279 "This changes Claude Code's instructions only: the model's own judgment and the permission system are unchanged.",
280 '/system-promt reset all restores every section.',
281 ].join('\n')
282}
283
284export const register: Register = on => {
285 on('session.start', async ($, e, next) => {
286 await $.command.register({
287 name: NAME,
288 description: 'Show, export or edit the system prompt',
289 argumentHint: '[list | export [path] | edit <section> | reset <section|all> | allow everything]',
290 })
291
292 // Both answers are cached from before this mod loaded; asked again, they
293 // pass the two hooks below on the next request.
294 $.ui.invalidate('prompt.context')
295 $.ui.invalidate('prompt.attachment')
296
297 return next(e)
298 })
299
300 on('prompt.context', async ($, e, next) => {
301 const answer = await next(e)
302 contextBlocks = answer.blocks
303
304 return answer
305 })
306
307 on('prompt.attachment', async ($, e, next) => {
308 const answer = await next(e)
309
310 if (answer.text !== null && e.agentId === undefined) {
311 const texts = injected.get(e.type) ?? []
312
313 if (!texts.includes(answer.text)) {
314 injected.set(e.type, [...texts, answer.text].slice(-20))
315 }
316 }
317
318 return answer
319 })
320
321 on('prompt.compose', async ($, e, next) => {
322 const composed = await next(e)
323 lastInput = e
324 originals = composed.sections
325 const overrides = await overridesOf($)
326 const sections: PromptComposeSection[] = []
327
328 for (const one of composed.sections) {
329 const path = overrides[one.id]
330 const text = path === undefined ? undefined : await edited($, path)
331
332 if (text === undefined) {
333 sections.push(one)
334 } else if (text.trim() !== '') {
335 sections.push({ ...one, text })
336 }
337 }
338
339 return { sections }
340 })
341
342 on('command.run', { command: NAME }, async ($, e) => {
343 const [verb = '', ...rest] = e.args.trim().split(/\s+/)
344 const argument = rest.join(' ')
345
346 if (verb === '') {
347 await update($, revision, n => n + 1)
348 await $.ui.open({ id: PANE, title: 'System prompt', focus: true })
349
350 return { text: 'System prompt pane opened.' }
351 }
352
353 if (verb === 'list') {
354 const list = await rows($)
355
356 return {
357 text: list
358 .map(one => `${one.isEdited ? '✎' : ' '} ${one.id} (${one.scope}, ${compact(one.text.length)} chars)`)
359 .join('\n'),
360 }
361 }
362
363 if (verb === 'export') {
364 return { text: await exportTo($, argument || 'system-prompt.md') }
365 }
366
367 if (verb === 'edit' && argument !== '') {
368 return { text: await edit($, argument) }
369 }
370
371 if (verb === 'reset' && argument !== '') {
372 return { text: await reset($, argument) }
373 }
374
375 if (verb === 'allow' && argument === 'everything') {
376 return { text: await allowEverything($) }
377 }
378
379 return { text: USAGE }
380 })
381
382 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
383 const table = $.ui.resolve(e)
384 const { Box, Button, Text } = table
385 await read($, revision)
386 const list = await rows($)
387 const picked = await read($, selected)
388 const row = list.find(one => one.id === picked) ?? list[0]
389
390 if (!row) {
391 return <Text dimColor>No system prompt sections.</Text>
392 }
393
394 const chars = list.reduce((sum, one) => sum + one.text.length, 0)
395 const editedCount = list.filter(one => one.isEdited).length
396
397 return (
398 <Box flexDirection="column">
399 <Text dimColor>
400 {list.length} sections, {compact(chars)} chars, {editedCount} edited
401 </Text>
402 {'Select' in table ? (
403 <table.Select
404 key="section"
405 label="Section"
406 autoFocus
407 value={row.id}
408 options={list.map(one => ({
409 value: one.id,
410 label: `${one.isEdited ? '✎ ' : ''}${one.id} (${one.scope}, ${compact(one.text.length)})`,
411 }))}
412 onSelect={(id: string) => update($, selected, () => id)}
413 />
414 ) : (
415 <Button
416 key="next"
417 label="Next section"
418 onPress={() => {
419 const after = list[(list.indexOf(row) + 1) % list.length]
420
421 return update($, selected, () => after?.id ?? null)
422 }}
423 />
424 )}
425 <Box>
426 <Button
427 key="edit"
428 label="Edit"
429 hotkey="e"
430 onPress={async () => {
431 $.ui.toast((await edit($, row.id)).split('\n')[0] ?? '')
432 }}
433 />
434 <Text> </Text>
435 <Button
436 key="reset"
437 label="Reset"
438 hotkey="r"
439 onPress={async () => {
440 $.ui.toast(await reset($, row.id))
441 }}
442 />
443 <Text> </Text>
444 <Button
445 key="export"
446 label="Export"
447 hotkey="x"
448 onPress={async () => {
449 $.ui.toast(await exportTo($, 'system-prompt.md'))
450 }}
451 />
452 <Text> </Text>
453 <Button
454 key="refresh"
455 label="Refresh"
456 hotkey="f"
457 onPress={() => update($, revision, n => n + 1)}
458 />
459 </Box>
460 <Text bold>
461 {row.id}
462 {row.isEdited ? ' (edited)' : ''}
463 </Text>
464 <Text wrap="wrap">{row.text.slice(0, SHOWN_CHARS)}</Text>
465 {row.text.length > SHOWN_CHARS && (
466 <Text dimColor>
467 {compact(row.text.length - SHOWN_CHARS)} more chars; Export shows the whole text.
468 </Text>
469 )}
470 </Box>
471 )
472 })
473}
474types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'system-promt': { selected: string | null; revision: number }
4 }
5}
6