SLOPSHOPPER

git-guard

Refuses git pushes, force pushes, rebases, amends, hard resets, `git add -A` and doc commits the person didn't ask for this turn, and strips Claude's co-author…

newguardcommandpromptprocess
v0.1.0no licenseupdated 2026-10-09zainokta/zstack/mods/git-guard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · git-guard
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(rm -rf build && git push --force origin main) ⎿ Denied by git-guard: git-guard: `git push --force origin main` refused: a force push needs the user to ask ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /git-guard ⎿ git-guard: git-guard is on. Use /git-guard off or /git-guard on. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

git-guard

Stops Claude from running git operations you didn't ask for in your latest prompt. It exists because of complaints like "stop committing the docs", "just push, why rebase", "commit all, I said all" and "don't add yourself as co-author".

  • git push runs only if your latest prompt says push, ship or deploy. A force push needs "force". A rebase (including pull --rebase), commit --amend, reset --hard, clean -f and checkout -- ./restore . each need their own words.
  • git add -A/./--all and git commit -a need "all" or "semua". Otherwise Claude is told to stage explicit paths.
  • git commit checks the index, plus anything the same command line stages first. If it would commit docs/**, plans/ or specs/ Markdown, ADRs, *handover*, CLAUDE.md or AGENTS.md, it is refused unless the prompt mentions docs, readme or ADR, or names the file.
  • "don't do any git command" refuses every git write for that turn. Reads such as git status still run.
  • It strips Claude's Co-Authored-By trailer and the "Generated with Claude Code" line from commit and PR messages. It also empties the engine's commit and PR attribution text. This stays on even while the guard is paused.
  • /git-guard off pauses the guard for the session. /git-guard on turns it back on.

Your latest prompt is the last one you typed or sent from your phone. Scripts (claude -p), notifications and other sessions don't count. Negations are read only roughly: "why rebase" and "don't push" don't count as asking, but other phrasings can.

The guard fails closed: if its own check breaks, the git command is refused (other commands still run). It reads the command text only, so aliases, scripts and bash -c get past it. It is a safety net, not a permission system.

Source 4 files
hooks/register.tsx 102 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { ASK, addedPaths, asks, fromRoot, gitCalls, isDocPath, refusal, stripAttribution } from './rules'
5import type { GitCall } from './rules'
6
7const lastPrompt = atom({ plugin: 'git-guard', key: 'prompt' } as const, '')
8const paused = atom({ plugin: 'git-guard', key: 'paused' } as const, false)
9
10const GIT = /\bgit\b/
11
12async function absDir($: EngineInterface, dir: string | null): Promise<string | undefined> {
13  if (dir === null) return undefined
14  if (dir === '~' || dir.startsWith('~/')) return `${(await $.env.get('HOME')) ?? ''}${dir.slice(1)}`
15  return dir
16}
17
18async function gitLines($: EngineInterface, cwd: string | undefined, args: string[]): Promise<string[]> {
19  const run = await $.process.run(['git', ...args], { cwd, timeoutMs: 8000 })
20  if (run.exitCode !== 0) throw new Error(`git ${args[0]} failed: ${run.stderr.trim().slice(0, 160)}`)
21  return run.stdout.split('\n').filter(l => l !== '')
22}
23
24/** Repo-root paths the commit at `at` would record: the index plus what the line stages first. */
25async function committedPaths($: EngineInterface, calls: GitCall[], at: number): Promise<string[]> {
26  const cwd = await absDir($, calls[at]!.dir)
27  const probe = await $.process.run(['git', 'rev-parse', '--show-prefix'], { cwd, timeoutMs: 8000 })
28  if (probe.exitCode !== 0) return [] // not a repo: the commit fails on its own
29  const prefix = probe.stdout.trim()
30  const { paths, broad, all } = addedPaths(calls, at)
31  const found = new Set(await gitLines($, cwd, ['diff', '--cached', '--name-only']))
32  for (const p of paths) found.add(fromRoot(prefix, p))
33  if (broad || all) for (const p of await gitLines($, cwd, ['diff', '--name-only', 'HEAD'])) found.add(p)
34  if (broad) for (const p of await gitLines($, cwd, ['ls-files', '--others', '--exclude-standard', '--full-name'])) found.add(p)
35  return [...found]
36}
37
38export const register: Register = on => {
39  on('session.start', async ($, e, next) => {
40    await $.command.register({
41      name: 'git-guard',
42      description: 'Pause (off) or resume (on) git-guard for this session',
43      argumentHint: 'on|off',
44    })
45    return next(e)
46  })
47
48  on('command.run', { command: 'git-guard' }, async ($, e) => {
49    const arg = e.args.trim().toLowerCase()
50    if (arg === 'off' || arg === 'on') {
51      await update($, paused, () => arg === 'off')
52      return { text: arg === 'off' ? 'git-guard paused for this session.' : 'git-guard is on.' }
53    }
54    return { text: `git-guard is ${(await read($, paused)) ? 'paused' : 'on'}. Use /git-guard off or /git-guard on.` }
55  })
56
57  // What the person asked this turn. Their own Enter or their phone; not a
58  // script, a notification or another session.
59  on('prompt.submit', async ($, e, next) => {
60    if (e.origin.kind === 'composer' || e.origin.kind === 'bridge') {
61      await update($, lastPrompt, () => e.text)
62    }
63    return next(e)
64  })
65
66  on('attribution.text', ($, e, next) => (e.kind === 'commit' || e.kind === 'pr' ? { text: '' } : next(e)))
67
68  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
69    const command = stripAttribution(e.command)
70    const pass = () => next(command === e.command ? e : { ...e, command })
71    if (!GIT.test(command) || (await read($, paused))) return pass()
72
73    const prompt = await read($, lastPrompt)
74    const calls = gitCalls(command)
75    for (const call of calls) {
76      const why = refusal(call, prompt)
77      if (why !== null) return { deny: `git-guard: ${why}` }
78    }
79
80    const at = calls.findIndex(c => c.sub === 'commit')
81    if (at >= 0 && !asks(prompt, ASK.docs)) {
82      const named = prompt.toLowerCase()
83      const docs = (await committedPaths($, calls, at))
84        .filter(isDocPath)
85        .filter(p => !named.includes((p.split('/').pop() ?? p).toLowerCase()))
86      if (docs.length > 0) {
87        const list = docs.slice(0, 15).join(', ') + (docs.length > 15 ? `, +${docs.length - 15} more` : '')
88        return {
89          deny: `git-guard: this commit would include docs/plans/notes the user didn't ask to commit: ${list}. Leave them out (\`git restore --staged <path>\`) and commit the rest; mention them to the user instead.`,
90        }
91      }
92    }
93    return pass()
94  }).catch(($, e, next) => {
95    // Fail closed for git (Jev: fail_closed 0.83); anything else goes on.
96    if (next.called) return next(e)
97    return e.tool === 'Bash' && GIT.test(e.command)
98      ? { deny: 'git-guard: its check failed, so this git command was not run. Retry, or the user can run /git-guard off.' }
99      : next(e)
100  })
101}
102
hooks/rules.ts 198 lines
1import { commands, joinDir, program } from './shell'
2
3export type GitCall = {
4  sub: string
5  args: string[]
6  /** Where git runs: the line's `cd`s, then `-C`; null = the session folder. */
7  dir: string | null
8}
9
10/** Every `git <sub>` on the line, in order. */
11export function gitCalls(line: string): GitCall[] {
12  const out: GitCall[] = []
13  for (const { words, dir } of commands(line)) {
14    if (program(words[0]) !== 'git') continue
15    let at = dir
16    let i = 1
17    while (i < words.length && words[i]!.startsWith('-')) {
18      const opt = words[i]!
19      if (opt === '-C') {
20        at = joinDir(at, words[i + 1])
21        i += 2
22      } else if (opt === '-c' || opt === '--git-dir' || opt === '--work-tree' || opt === '--namespace') {
23        i += 2
24      } else {
25        i += 1
26      }
27    }
28    const sub = words[i]
29    if (sub !== undefined) out.push({ sub, args: words.slice(i + 1), dir: at })
30  }
31  return out
32}
33
34// ---- What the prompt asked for ----------------------------------------------
35
36const NEGATION = /\b(don'?t|dont|do not|never|no|not|stop|why|jangan|gak|ga|nggak|tidak|tanpa|without)\b/i
37
38/**
39 * True when the prompt names the thing and the few words before it, in the same
40 * clause, don't negate it: "just push" yes, "why rebase" and "don't push" no.
41 */
42export function asks(prompt: string, pattern: RegExp): boolean {
43  const global = new RegExp(pattern.source, 'gi')
44  for (const m of prompt.matchAll(global)) {
45    const clause = prompt.slice(0, m.index).split(/[.,!?;:\n]/).pop() ?? ''
46    const before = clause.trim().split(/\s+/).slice(-3).join(' ')
47    if (!NEGATION.test(before)) return true
48  }
49  return false
50}
51
52export const ASK = {
53  push: /\b(push|ship|deploy)\w*/,
54  force: /\bforce|\bpush\s+-f\b/,
55  rebase: /\brebas\w*/,
56  amend: /\bamend\w*/,
57  resetHard: /\breset\s+(--)?hard\b|\bhard[- ]reset\b/,
58  clean: /\bgit\s+clean\b|\buntracked\b/,
59  discard: /\bdiscard\w*|\bbuang\w*|\bgit\s+(checkout|restore)\s+(--\s+)?\./,
60  all: /\ball\b|\bsemua\b/,
61  docs: /\b(docs?|documentation|readme|adrs?|dokumen\w*)\b/,
62}
63
64/** "don't DO any git command", "jangan pakai git": no git writes this turn. */
65const NO_GIT = /\b(don'?t|dont|do not|never|jangan)\s+((do|run|use|touch|make|execute|any|pakai|pake|jalankan|lakukan)\s+){0,2}git\b/i
66
67const READ_ONLY = new Set([
68  'status', 'diff', 'log', 'show', 'blame', 'rev-parse', 'ls-files', 'ls-tree', 'grep', 'describe',
69  'shortlog', 'cat-file', 'merge-base', 'help', 'version', 'reflog', 'rev-list', 'whatchanged',
70])
71
72function readsOnly({ sub, args }: GitCall): boolean {
73  if (READ_ONLY.has(sub)) return sub !== 'reflog' || args[0] === undefined || args[0] === 'show'
74  if (sub === 'branch') return args.every(a => /^(-v+|-a|-r|--list|--show-current|--contains|--merged|--no-merged)$/.test(a))
75  if (sub === 'remote') return args.length === 0 || args[0] === '-v' || args[0] === 'show' || args[0] === 'get-url'
76  if (sub === 'stash') return args[0] === 'list' || args[0] === 'show'
77  if (sub === 'config') return args.some(a => a === '--get' || a === '--list' || a === '-l' || a === '--get-all')
78  if (sub === 'worktree') return args[0] === 'list'
79  return false
80}
81
82const short = (args: readonly string[], letter: string) =>
83  args.some(a => new RegExp(`^-[a-zA-Z]*${letter}[a-zA-Z]*$`).test(a))
84
85/** Why this call is refused given the prompt, or null when it may run. */
86export function refusal(call: GitCall, prompt: string): string | null {
87  const { sub, args } = call
88  const named = `git ${[sub, ...args].join(' ')}`.slice(0, 120)
89  if (NO_GIT.test(prompt) && !readsOnly(call)) {
90    return `\`${named}\` refused: the user said not to run git commands this turn. Do not run git; tell the user what you would run.`
91  }
92  if (sub === 'push') {
93    const isForce = args.some(a => a === '--force' || a.startsWith('--force-with-lease') || a === '--force-if-includes' || /^\+/.test(a)) || short(args, 'f')
94    if (isForce && !asks(prompt, ASK.force)) {
95      return `\`${named}\` refused: a force push needs the user to ask for it this turn ("force push"). Ask the user instead.`
96    }
97    if (!asks(prompt, ASK.push)) {
98      return `\`${named}\` refused: the user didn't ask to push this turn. Stop after committing and say it's ready to push.`
99    }
100  }
101  if (sub === 'rebase' || (sub === 'pull' && args.some(a => a === '--rebase' || a.startsWith('--rebase=') || a === '-r'))) {
102    if (!asks(prompt, ASK.rebase)) {
103      return `\`${named}\` refused: the user didn't ask for a rebase this turn. Don't rebase; if a push is rejected, report it and ask.`
104    }
105  }
106  if (sub === 'commit' && args.includes('--amend') && !asks(prompt, ASK.amend)) {
107    return `\`${named}\` refused: the user didn't ask to amend this turn. Make a new commit instead.`
108  }
109  if (sub === 'reset' && args.includes('--hard') && !asks(prompt, ASK.resetHard)) {
110    return `\`${named}\` refused: \`git reset --hard\` throws away work and the user didn't ask for it this turn.`
111  }
112  if (sub === 'clean' && (args.includes('--force') || short(args, 'f')) && !asks(prompt, ASK.clean)) {
113    return `\`${named}\` refused: \`git clean -f\` deletes untracked files and the user didn't ask for it this turn.`
114  }
115  const dropsAll = (sub === 'checkout' && args.includes('.')) ||
116    (sub === 'restore' && args.includes('.') && !(args.includes('--staged') && !args.includes('--worktree')))
117  if (dropsAll && !asks(prompt, ASK.discard)) {
118    return `\`${named}\` refused: it discards every uncommitted change and the user didn't ask for that this turn.`
119  }
120  if (isBroadAdd(call) && !asks(prompt, ASK.all)) {
121    return `\`${named}\` refused: the user didn't say to commit "all". Stage explicit paths (\`git add <file> ...\`) for the files you changed for this task.`
122  }
123  return null
124}
125
126/** `git add -A/--all/./:/`, `git commit -a`: stages everything. */
127export function isBroadAdd({ sub, args }: GitCall): boolean {
128  if (sub === 'add') return args.some(a => a === '-A' || a === '--all' || a === '.' || a === ':/' || a === '*' || /^-[a-zA-Z]*A[a-zA-Z]*$/.test(a))
129  if (sub === 'commit') return args.includes('--all') || short(args, 'a')
130  return false
131}
132
133// ---- Docs that shouldn't ride along in a commit -----------------------------
134
135const DOC_EXT = /\.(md|mdx|txt|rst|adoc)$/i
136
137/** A path (from the repo root) that looks like a plan, spec, handover, ADR or agent note. */
138export function isDocPath(path: string): boolean {
139  const p = path.replace(/^\.\//, '')
140  const base = p.split('/').pop() ?? p
141  if (/^docs?\//i.test(p)) return true
142  if (/(^|\/)(plans|specs)\//i.test(p) && DOC_EXT.test(p)) return true
143  if (/(^|\/)(adrs?|decisions)\//i.test(p) && DOC_EXT.test(p)) return true
144  if (/^adr[-_ ]?\d+/i.test(base)) return true
145  if (/handover/i.test(base)) return true
146  return /^(CLAUDE|AGENTS)(\.local)?\.md$/i.test(base)
147}
148
149/** Paths this commit's docs check should look at besides the index. */
150export function addedPaths(calls: readonly GitCall[], commitAt: number): { paths: string[]; broad: boolean; all: boolean } {
151  const paths: string[] = []
152  let broad = false
153  const VALUED = new Set(['-m', '--message', '-F', '--file', '-C', '-c', '--author', '--date', '-t', '--template', '--fixup', '--squash', '--reuse-message', '--reedit-message', '--pathspec-from-file'])
154  calls.forEach((call, i) => {
155    if (i > commitAt) return
156    if (call.sub === 'add' && i < commitAt) {
157      if (isBroadAdd(call)) broad = true
158      else paths.push(...call.args.filter(a => !a.startsWith('-')))
159    }
160    if (i === commitAt) {
161      for (let k = 0; k < call.args.length; k += 1) {
162        const a = call.args[k]!
163        if (VALUED.has(a)) k += 1
164        else if (!a.startsWith('-')) paths.push(a)
165      }
166    }
167  })
168  const commit = calls[commitAt]
169  return { paths, broad, all: commit !== undefined && isBroadAdd(commit) }
170}
171
172/** `prefix` + `path`, with `.` and `..` folded: a repo-root path. */
173export function fromRoot(prefix: string, path: string): string {
174  const parts: string[] = []
175  for (const part of `${prefix}${path}`.split('/')) {
176    if (part === '' || part === '.') continue
177    if (part === '..') parts.pop()
178    else parts.push(part)
179  }
180  return parts.join('/')
181}
182
183// ---- Attribution --------------------------------------------------------------
184
185/**
186 * The command with Claude's Co-Authored-By trailer and "Generated with Claude
187 * Code" line taken out of a commit or PR message. Quotes are left in place.
188 */
189export function stripAttribution(command: string): string {
190  if (!/\bgit\b[\s\S]*\bcommit\b|\bgh\s+pr\s+(create|edit)\b/.test(command)) return command
191  const out = command
192    .replace(/[ \t]+-m[ \t]*(["'])\s*Co-Authored-By:[^"'\n]*(claude|anthropic)[^"'\n]*\1/gi, '')
193    .replace(/[ \t]+-m[ \t]*(["'])\s*(🤖\s*)?Generated with \[?Claude Code[^"'\n]*\1/gi, '')
194    .replace(/^[ \t]*Co-Authored-By:[^\n"']*(claude|anthropic)[^\n"']*/gim, '')
195    .replace(/^[ \t]*(🤖[ \t]*)?Generated with \[?Claude Code[^\n"']*/gim, '')
196  return out === command ? command : out.replace(/\n{3,}/g, '\n\n')
197}
198
hooks/shell.ts 189 lines
1// A shell command line read well enough to find the commands in it: quotes,
2// escapes, separators (&& || ; | & newline), ( ) subshells and `cd`. Heredoc
3// bodies are cut out first; $( ... ) stays inside its word. Not a full shell.
4
5export type Command = {
6  /** The words, unquoted, with redirects, VAR=value, sudo and the like removed. */
7  words: string[]
8  /** Where a `cd`/`pushd` earlier on the line moved to; null = the session folder. */
9  dir: string | null
10}
11
12const HEREDOC = /<<-?[ \t]*(['"]?)([A-Za-z_]\w*)\1([^\n]*)\n([\s\S]*?)\n[ \t]*\2[ \t]*(?=\n|$)/g
13
14/** The command with heredoc bodies removed, and the bodies. */
15export function stripHeredocs(input: string): { text: string; bodies: string[] } {
16  const bodies: string[] = []
17  const text = input.replace(HEREDOC, (_m, q: string, tag: string, rest: string, body: string) => {
18    bodies.push(body)
19    return `<<${q}${tag}${q}${rest}`
20  })
21  return { text, bodies }
22}
23
24type Raw = { words: string[]; depth: number }
25
26function split(input: string): Raw[] {
27  const out: Raw[] = []
28  let words: string[] = []
29  let word = ''
30  let inWord = false
31  let depth = 0
32  let segDepth = 0
33  const endWord = () => {
34    if (inWord) words.push(word)
35    word = ''
36    inWord = false
37  }
38  const endSeg = () => {
39    endWord()
40    if (words.length > 0) out.push({ words, depth: segDepth })
41    words = []
42    segDepth = depth
43  }
44  for (let i = 0; i < input.length; i += 1) {
45    const c = input[i]!
46    if (c === '\\') {
47      if (i + 1 < input.length && input[i + 1] !== '\n') {
48        word += input[i + 1]
49        inWord = true
50      }
51      i += 1
52    } else if (c === "'") {
53      const j = input.indexOf("'", i + 1)
54      const end = j < 0 ? input.length : j
55      word += input.slice(i + 1, end)
56      inWord = true
57      i = end
58    } else if (c === '"') {
59      let j = i + 1
60      while (j < input.length && input[j] !== '"') {
61        if (input[j] === '\\' && j + 1 < input.length && '"\\$`'.includes(input[j + 1]!)) {
62          word += input[j + 1]
63          j += 2
64        } else {
65          word += input[j]
66          j += 1
67        }
68      }
69      inWord = true
70      i = j
71    } else if (c === '$' && input[i + 1] === '(') {
72      let j = i + 2
73      let d = 1
74      while (j < input.length && d > 0) {
75        if (input[j] === '(') d += 1
76        else if (input[j] === ')') d -= 1
77        j += 1
78      }
79      word += input.slice(i, j)
80      inWord = true
81      i = j - 1
82    } else if (c === '#' && !inWord) {
83      while (i + 1 < input.length && input[i + 1] !== '\n') i += 1
84    } else if (c === ' ' || c === '\t') {
85      endWord()
86    } else if (c === '\n' || c === ';') {
87      endSeg()
88    } else if (c === '&' && (input[i - 1] === '>' || input[i + 1] === '>')) {
89      word += c
90      inWord = true
91    } else if (c === '&' || c === '|') {
92      if (input[i + 1] === c) i += 1
93      endSeg()
94    } else if (c === '(' && !inWord) {
95      endSeg()
96      depth += 1
97      segDepth = depth
98    } else if (c === ')') {
99      endSeg()
100      depth = Math.max(0, depth - 1)
101      segDepth = depth
102    } else {
103      word += c
104      inWord = true
105    }
106  }
107  endSeg()
108  return out
109}
110
111const PREFIXES = new Set(['command', 'exec', 'env', 'nohup', 'time', 'then', 'do', 'else', '!', 'builtin'])
112const SUDO_VALUE = new Set(['-u', '-g', '-C', '-D', '-h', '-p', '-r', '-t', '-T', '-U'])
113const REDIRECT = /^(\d*|&)(>>?|<<?-?|<)/
114
115function normalize(input: readonly string[]): string[] {
116  const words: string[] = []
117  for (let i = 0; i < input.length; i += 1) {
118    const w = input[i]!
119    const m = REDIRECT.exec(w)
120    if (m) {
121      if (m[0] === w) i += 1 // `> file`: drop the target too
122      continue
123    }
124    words.push(w)
125  }
126  for (;;) {
127    const first = words[0]
128    if (first === undefined) break
129    if (/^[A-Za-z_]\w*=/.test(first) || PREFIXES.has(first)) {
130      words.shift()
131    } else if (first === 'sudo' || first === 'doas') {
132      words.shift()
133      while (words[0]?.startsWith('-')) {
134        const opt = words.shift()!
135        if (SUDO_VALUE.has(opt)) words.shift()
136      }
137    } else if (first === 'nice') {
138      words.shift()
139      if (words[0] === '-n') words.splice(0, 2)
140      else if (/^-\d+$/.test(words[0] ?? '')) words.shift()
141    } else {
142      break
143    }
144  }
145  return words
146}
147
148export function joinDir(dir: string | null, arg: string | undefined): string {
149  if (arg === undefined || arg === '~' || arg.startsWith('/') || arg.startsWith('~/')) return arg ?? '~'
150  return dir === null ? arg : `${dir}/${arg}`
151}
152
153/** Every simple command on the line, in order, each with the folder it runs in. */
154export function commands(line: string): Command[] {
155  const raws = split(stripHeredocs(line).text)
156  const out: Command[] = []
157  let dir: string | null = null
158  let depth = 0
159  const scopes: (string | null)[] = []
160  const pushed: (string | null)[] = []
161  for (const raw of raws) {
162    while (depth < raw.depth) {
163      scopes.push(dir)
164      depth += 1
165    }
166    while (depth > raw.depth) {
167      dir = scopes.pop() ?? null
168      depth -= 1
169    }
170    const words = normalize(raw.words)
171    const [cmd, arg] = words
172    if (cmd === 'cd') {
173      dir = arg === '-' ? '-' : joinDir(dir, arg)
174    } else if (cmd === 'pushd') {
175      pushed.push(dir)
176      dir = joinDir(dir, arg)
177    } else if (cmd === 'popd') {
178      dir = pushed.length > 0 ? (pushed.pop() ?? null) : '-'
179    }
180    if (words.length > 0) out.push({ words, dir })
181  }
182  return out
183}
184
185/** The program's bare name: `/usr/bin/git` and `\git` are `git`. */
186export function program(word: string | undefined): string {
187  return (word ?? '').replace(/^\\/, '').replace(/^.*\//, '')
188}
189
types/index.d.ts 8 lines
1export type GitGuardPrompt = string
2
3declare module 'claude-code' {
4  interface PluginState {
5    'git-guard': { prompt: GitGuardPrompt; paused: boolean }
6  }
7}
8