SLOPSHOPPER

reins-status

Read-only view of the reins hold queue inside Claude Code: a status line count and a band above the prompt. It shows the queue and never answers it.

newpanebandcommandtoaststatus
★ 1v0.1.0MITupdated 2026-10-02manishkumar/reins/mods/reins-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · reins-status
│ ┃ reins holds ✕ › fix the failing auth test and add an audit log call │ ┃ Nothing is held. │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /reins │ ⎿ reins-status: Nothing is held. The reins pane is open. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · reins holds
Nothing is held.
README

🐎 reins

Keep control of Claude Code agents you aren't watching

Hold risky actions for your approval · block what must never run · see every agent on one screen · nudge one mid-run

CI License: MIT Node ≥ 18 Dependencies: zero Network calls: zero

Install · First run · What it does · Before you rely on it · Commands

<img src="assets/cockpit-review-queue.svg" alt="reins watch after an overnight run: two held actions and a worked-around guard under NEEDS YOU, six agents listed with a looping one first, and the detail of a held terraform apply showing its rule, reason, session, how long it has waited and the full proposed command with the matched line lifted above it" width="960">

Local-first. No daemon, no backend, no account. Nothing leaves your machine.


Why this exists

An agent works for hours while you do something else, or overnight. At some point it reaches a deploy, a publish or a push to main. Without reins you have two choices: let it run actions nobody approved, or stay at the terminal to answer every prompt.

reins gives you a third, built from Claude Code hooks. The risky action is parked, the agent carries on with other work, and you decide when you are back:

# the agent, at 02:14, nobody watching
  ⏺ Bash(npm publish)
  [reins] ⏸ HELD  npm publish
    approve: reins approve ab12cd34   ·   see all: reins pending

# you, in the morning
  $ reins pending
    ab12cd34  7h  3b9f2a1c  Bash  npm publish  [bash-npm-publish]
  $ reins approve ab12cd34
  ✓ Approved ab12cd34 (Bash: npm publish)

The approval clears that exact call, once. It permits the retry and does not run anything itself: a still-running agent is told to retry, and a session that has ended needs resuming.

WhenWhatHardness
Before a tool runsHold: park the action in a queue until you approve itYour call, later
Before a tool runsGuard: deny a command or path, or ask you at the promptHard veto, or your call
Any timeCockpit: every agent and everything waiting on you, on one screenObserve and decide
During the runSteer: add a one-line note the agent reads at its next tool callSoft. The model weighs it
After each toolLoop alarm: warn when the same call repeats in a rowWarn
When a turn endsClaim check: compare "done" with the tests and builds the session ranReport
AlwaysCapture: every run in a SQLite file you ownObserve

The story behind it: *I Built a Tool to Steer Running AI Agents. It Taught Me Where Their Real Cost Is.*


Install

npm install -g @manishky/reins
reins version

npm install -g github:manishkumar/reins works too. For a local checkout: git clone, then npm install && npm run build && npm link.

Requires Node ≥ 18. Capture needs SQLite, which is built in from Node 22.5. Steering, guards and holds work without it. See compatibility.

First run

cd your-project
reins init          # creates .reins/ and merges the hooks into .claude/settings.json

Restart Claude Code in the project so it loads the hooks. reins init merges into your settings and never overwrites them. reins init --print prints the block instead, and reins init --local writes to settings.local.json.

Then add a rule for something you want to sign off, start any task in Claude Code, and open the cockpit in another terminal:

reins guard add bash "npm publish" --hold   # park it for approval
reins watch                                 # every agent, and everything waiting on you

Run reins doctor to check the setup: it reports the hooks, the Claude Code version, and whether a hook has run since the settings changed.


What it does

Hold: approval for the run nobody is watching

A hold rule parks the proposed action in a queue and lets the agent carry on with other work. The action does not run until you approve it.

reins guard add bash "git push" --hold        # hold any push for approval

reins pending                                 # what did the agents want to do?
#   ab12cd34  7h  3b9f2a1c  Bash  git push origin main  [bash-git-push]

reins approve ab12cd34                        # clears that exact call, once
reins deny ab12cd34 --steer "open a PR instead of pushing to main"

An approval is bound to one proposal: the same input, from the same session and directory, one time. A changed retry parks again.

<img src="assets/hold-queue.svg" alt="The full hold-queue loop: a hold rule parks the agent's npm install and shows you a one-line HELD notice with the approve command, reins pending lists it, reins approve signs it off, and the agent's retry runs at its next tool boundary" width="720">

Transports, breach reporting and every caveat: docs/holds.md.

The cockpit: reins watch

One screen shows every agent in the repo and everything waiting on you. From it you approve or deny held actions and steer one agent or all of them. It is the control surface for unattended and headless runs and for several agents at once.

<img src="assets/cockpit-agent.svg" alt="reins watch with a long-running agent selected: the detail pane shows what it was asked, its branch, its claim check verdict, the files it edited by directory, the commands it ran, and its trajectory newest first" width="960">

<img src="assets/cockpit-approve.svg" alt="reins watch approve dialog: 'Approve this exact call, once?' with the rule, reason, session, directory and the exact input, the matched line marked, and y to approve or esc to cancel" width="960">

  • Looping agents are listed first, then agents with a held action, then the rest newest first.
  • Approving in the cockpit runs the same code as reins approve. The action is re-read when you press y, and nothing is approved if it changed.
  • The selection never moves on its own, so a new hold cannot slide under your cursor.
  • Nothing listens on a port. reins watch --once prints a plain snapshot for scripts.

Keys, layout and caveats: docs/watch.md.

Guard

A guard matches a Bash command, a file path or a tool name before the call runs. Besides hold, it can deny the call outright or ask you at the prompt:

reins guard add bash "psql.*production"       # deny: the call does not run
reins guard add bash "git push" --ask         # ask: Claude Code shows you its permission prompt
reins guard add bash "npm publish" --hold     # hold: park it for your later approval
reins guard add path "infra/**"               # block writes to paths
reins guard add tool "mcp__stripe__*" --ask   # match MCP tools by name
reins guard list

A deny holds under --permission-mode bypassPermissions. reins ships a default denylist: recursive rm outside build and scratch directories, force pushes, git reset --hard, DROP/TRUNCATE, curl … | sh, and writes to .env* and .git/**. reins scan proposes rules from your repo's own manifests, and reins policy upgrade refreshes shipped rules while keeping yours.

<img src="assets/guard-list.svg" alt="reins guard list output: the default denylist plus a hold rule, each with its hardness (deny/ask/hold), pattern, and reason" width="820">

Details, the measured false-positive rate, and reins audit --guards: docs/guards.md.

Steer

reins steer "<message>" queues a note for the next tool call. Two steers before that call both arrive. If the agent is already finishing and there is no next tool call, the Stop hook delivers the note, so a queued steer is not lost.

# terminal 1: the agent is mid-task and drifting
  ⏺ Edit(src/login/flow.ts)

# terminal 2: you, without ending the run
  $ reins steer "focus on the token refresh path, leave the login flow alone"
  ✓ queued — lands at the agent's next tool call

# terminal 1: next tool call, same run, context intact
  ⏺ [reins — live steering from the developer] folded in
  ⏺ Edit(src/auth/refresh.ts)

Treat a steer as the detail you forgot to put in the prompt. A steer that contradicts the prompt is weighed down by the model. For a hard "never do X", use a guard.

With several agents in one repo, reins steer asks which one you mean, or takes --session <name>.

<img src="assets/steer-picker.svg" alt="reins steer with several live sessions: a picker lists each agent by name with its status and last tool call, and asks where the steer should land" width="820">

Details and caveats: docs/steering.md.

Loop alarm

When the agent runs the same tool with identical input three times in a row, reins adds a warning at that tool call and records the loop. Re-running npm test after each edit does not count; the same call with nothing in between does. reins loops lists the sessions, and "loopThreshold" in .reins/config.json tunes it.

Claim check and footprint

When a turn ends, reins compares what the session did with its own tool calls:

[reins] Claim check: the last test run failed (npm test). reins lastrun lists the calls.

The verdicts are failed, stale (files edited after the last run), unverified (edits and no run), unknown (a run whose result reins cannot see) and verified. It reports and never blocks. The footprint lists what a session edited and ran beside the prompt it was given, and leaves the comparison to you.

What it can and cannot see: docs/claim-check.md.

Capture and the report

reins lastrun           # a readable account of the last run
reins sessions          # recent sessions, by title and name
reins audit             # every gate decision
reins report --open     # one self-contained HTML file, no network requests

Runs are stored in .reins/runs.db, three tables you can query with SQL. REINS_NO_SQLITE=1 turns capture off; steering, guards and holds keep working.

More: docs/capture.md.

Inside Claude Code: the status mod (experimental)

reins init --mod installs a read-only Claude Code mod. /reins opens a pane beside the conversation with every hold and its full input. The status line shows the count. It has no approve button: approving stays in reins approve and reins watch.

Caveats: docs/mods.md.


Before you rely on it

These are the limits that decide whether reins fits your use. Each links to the full text.

  • Guards are speed bumps, not a sandbox. They match the form of a command, not its intent. An agent blocked from rm -rf foo can delete another way. For a determined or adversarial agent, use OS-level sandboxing. docs/guards.md
  • Steering lands at the next tool call, and the model weighs it. It is added spec, and it does not reverse the prompt. docs/steering.md
  • A hold uses deny-and-queue by default. The defer transport is opt-in, because Claude Code honors it only in print mode and only for a solo tool call. A held action that ran anyway is reported as a HOLD BREACH after the fact. docs/holds.md
  • A crashing hook fails open. A bug in reins never stops your agent, which also means a guard does not run if the hook itself errors. The hold gate is the exception: if parking fails, the call is denied.
  • Claude Code older than 2.0.56 loads no hooks from a settings file that names PostToolUseFailure. reins init leaves that hook out unless it can confirm your version, and reins doctor tells you whether a hook has actually run. docs/compatibility.md
  • The claim check reads command text and exit status. A passing run is the agent's own run. It does not mean the tests cover the change. docs/claim-check.md
  • The mod API is early access and can change in any Claude Code release. docs/mods.md

Local-first

reins makes zero network calls. No telemetry and no account. Your trajectories live in a SQLite file on your disk that you can read, query, back up or delete. The steering queue, the policy, the hold queue and the decisions are plain files under .reins/, so they work without the database.

Security / threat model

.reins/steering.txt is security-sensitive: write access to it equals steering access. Anything that can write that file can inject context into your running agent at its next tool call. reins creates .reins/ as 0700 (owner-only) and git-ignores it, but be deliberate:

  • Do not let an untrusted or automated writer feed it. A CI job, a shared script, or any process you don't fully control writing to .reins/steering.txt is a hijack path into your agent. Treat write access to that file as you'd treat write access to your prompts.
  • Steering is a soft channel — the model still weighs it and resists outright contradictions — so this is defense-in-depth, not a sole control. v1 ships no signing on the steering file; the mitigations are filesystem permissions (0700) and not pointing untrusted writers at it.
  • .reins/decided/ is approval access. One-shot hold decisions — approvals and refusals — are files there; anything that can write them can pre-approve (or fake-refuse) a parked action. Same posture, same mitigations.

Guards, separately, are not a containment boundary (see what guards are and are not).

A Claude Code mod runs above reins. From Claude Code 2.1.287, a mod's tool.call hook runs before any PreToolUse command hook, reins included (measured in docs/mods-probe.md). A mod can rewrite a command before reins reads it, in which case reins matches, parks and approves the rewritten command, the one that would run. A mod can also answer a tool call itself, and then no PreToolUse hook runs and reins sees nothing. A mod is code you installed in your own Claude Code, so this is the same trust as any other local code. reins does not detect it.

A crashing hook fails open (the agent proceeds) so a bug in reins can never wedge your agent — which also means guards are best-effort if the hook itself errors.


Command reference

reins init [--print|--local]     Set up .reins/ and wire (or print) the hooks
reins init --mod                 Also install the read-only status mod
reins init --failure-hook        Write PostToolUseFailure when the Claude Code
                                 version can't be read (needs 2.0.56+)
reins uninstall [--purge]        Remove the hooks (--purge also drops .reins/)
reins doctor                     Diagnose your setup
reins steer "<msg>" [--replace]  Queue steering for the next tool call (appends;
                                 several live agents + a TTY → a picker asks which)
reins steer "<msg>" --session <id|name>   Target one agent (id/prefix/name/mnemonic)
reins steer "<msg>" --broadcast  Skip the picker; whichever agent moves next gets it
reins steer [--clear]            Show / clear pending steering
reins name <session> "<label>"   Name a session; --clear reverts to the auto mnemonic
reins guard list|add|remove|reset    (add takes --ask to escalate, --hold to park)
reins scan [--accept]            Propose rules for what THIS repo can destroy
reins policy upgrade [--apply]   Refresh shipped rules, keeping yours and your edits
reins policy version             Your policy generation vs the shipped one
reins pending                    List actions parked by hold rules
reins approve <id>               Approve a parked action (one-shot, exact call)
reins deny <id> [--steer "..."]  Refuse a parked action, optionally steer instead
reins audit [session] [--json]   Every gate decision (deny/ask/hold/allow/breach/bypass)
reins audit --guards [--json]    Were the guards right? Every denial ever recorded,
                                 scored: stale rules, and vetoes worked around anyway
reins lastrun [session]          Readable account of a run (id prefix or name)
reins sessions [-n N]            List recent sessions (with names)
reins watch [-n SECS] [--once] [--quiet]  Cockpit: approve/deny holds, steer agents
reins report [--open] [-o FILE]  Self-contained local HTML report of all runs
reins loops                      Sessions where the agent looped
reins hook pre-tool|post-tool|post-tool-failure|stop   (invoked by Claude Code, not you)

Documentation

SteeringDelivery, the two caveats, several agents in one repo
GuardsRules, --ask, the default denylist, what guards cannot do, audit --guards, scan, policy upgrade
HoldsThe approval queue, transports, one-shot approvals, HOLD BREACH
The cockpitreins watch: panes, keys, approving, narrow terminals
Claim check and footprintVerdicts, and what the check can and cannot see
Capture and the reportlastrun, sessions, report, the SQLite schema
The status modThe /reins pane inside Claude Code
CompatibilityNode versions, Claude Code versions, the PostToolUseFailure hook
How it worksThe hooks, the files under .reins/, testing a hook by hand
SPEC.mdFile formats and decision semantics, vendor-neutral

Contributing

See CONTRIBUTING.md. Keep it small and sharp.


MIT · no daemon, no backend, no account — just hooks, and files you own

Source 2 files
hooks/register.tsx 259 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Held } from '../types'
5
6/**
7 * The reins hold queue, shown inside Claude Code. Read-only on purpose.
8 *
9 * It reads `.reins/pending/*.json` (the same plain files `reins pending`
10 * reads). A count sits in the status line and in one line above the prompt,
11 * a new hold is announced once, and `/reins` opens a pane beside the
12 * conversation with each hold's full input. The queue is
13 * the repository's: holds from other sessions are listed too, and this
14 * session's are marked. It has no approve or deny control, and it hooks no
15 * `tool.call`: approvals go through `reins approve` and the `reins watch`
16 * cockpit only (CLAUDE.md, invariant 13). The mod engine skips a hook that
17 * fails or runs out of time, so a gate here would fail open
18 * (docs/mods-probe.md). A view that fails shows nothing, which is safe.
19 */
20
21const PANE = 'reins'
22const PANE_TITLE = 'reins holds'
23/** How much of one input the pane carries. The rest is in `reins watch`. */
24const MAX_LINES = 150
25const MAX_LINE = 400
26/** How far up from the session's root to look for `.reins`. */
27const MAX_UP = 12
28const REFRESH_MS = 5000
29
30const held = atom({ plugin: 'reins-status', key: 'held' } as const, [] as readonly Held[])
31
32/** Text from an agent run is data. Control characters never reach the terminal. */
33export const clean = (s: string, max: number): string => {
34  const flat = s
35    .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(\x07|\x1b\\)?/g, '')
36    .replace(/[\x00-\x1f\x7f-\x9f​-‏‪-‮⁦-⁩]/g, ' ')
37    .replace(/\s+/g, ' ')
38    .trim()
39
40  return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat
41}
42
43const describe = (tool: string, input: unknown): string => {
44  if (input !== null && typeof input === 'object') {
45    const o = input as Record<string, unknown>
46    for (const k of ['command', 'file_path', 'path', 'url']) {
47      if (typeof o[k] === 'string') {
48        return o[k]
49      }
50    }
51  }
52
53  return tool
54}
55
56/** One line of an input for the pane: indentation kept, control characters gone. */
57const cleanLine = (s: string): string => {
58  const flat = s
59    .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(\x07|\x1b\\)?/g, '')
60    .replace(/\t/g, '  ')
61    .replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/g, ' ')
62    .trimEnd()
63
64  return flat.length > MAX_LINE ? `${flat.slice(0, MAX_LINE - 1)}…` : flat
65}
66
67/** The whole input as lines, and how many lines were left out. */
68export const inputLines = (text: string): { lines: string[]; cut: number } => {
69  const all = text.replace(/\r\n?/g, '\n').split('\n')
70
71  return { lines: all.slice(0, MAX_LINES).map(cleanLine), cut: Math.max(0, all.length - MAX_LINES) }
72}
73
74const clock = (ts: number): string => (ts > 0 ? new Date(ts).toTimeString().slice(0, 5) : '')
75
76/** One pending file as a row, or null when it is not a parked action. */
77export const parse = (text: string): Held | null => {
78  try {
79    const p = JSON.parse(text) as Record<string, unknown>
80    if (typeof p.id !== 'string' || typeof p.tool !== 'string') {
81      return null
82    }
83    const ts = Date.parse(String(p.ts))
84    const { lines, cut } = inputLines(describe(p.tool, p.input))
85
86    return {
87      id: clean(p.id, 16),
88      session: typeof p.session_id === 'string' ? p.session_id : '',
89      tool: clean(p.tool, 24),
90      rule: clean(String(p.rule_id ?? ''), 32),
91      what: clean(describe(p.tool, p.input), 70),
92      ts: Number.isFinite(ts) ? ts : 0,
93      reason: clean(String(p.reason ?? ''), 200),
94      cwd: clean(String(p.cwd ?? ''), 200),
95      lines,
96      cut,
97    }
98  } catch {
99    return null
100  }
101}
102
103/** The directories from `dir` up to the filesystem root, nearest first. */
104export const upward = (dir: string): string[] => {
105  const out: string[] = []
106  let at = dir.replace(/[\\/]+$/, '')
107  for (let i = 0; i < MAX_UP; i++) {
108    out.push(at === '' ? '/' : at)
109    const cut = Math.max(at.lastIndexOf('/'), at.lastIndexOf('\\'))
110    if (cut < 0) {
111      break
112    }
113    at = at.slice(0, cut)
114    if (at === '' || /^[A-Za-z]:$/.test(at)) {
115      out.push(at === '' ? '/' : `${at}\\`)
116      break
117    }
118  }
119
120  return out
121}
122
123/**
124 * The folder holding `.reins`, found the way reins finds it: the nearest one
125 * at or above where the session stands. A session started in a subdirectory
126 * still sees its repository's queue.
127 */
128const pendingDir = async ($: EngineInterface): Promise<string | null> => {
129  for (const dir of upward(await $.session.root())) {
130    const reins = `${dir.replace(/[\\/]$/, '')}/.reins`
131    const found = await $.fs.stat(reins).catch(() => null)
132    if (found?.kind === 'dir') {
133      return `${reins}/pending`
134    }
135  }
136
137  return null
138}
139
140// The ids already announced. Null until the first read, which announces
141// nothing: holds that were waiting when the session started are in the band.
142let known: Set<string> | null = null
143
144const refresh = async ($: EngineInterface): Promise<void> => {
145  let rows: Held[] = []
146  try {
147    const dir = await pendingDir($)
148    if (dir !== null) {
149      const entries = await $.fs.list(dir)
150      const files = entries.filter(f => f.kind === 'file' && f.name.endsWith('.json')).slice(0, 50)
151      const texts = await Promise.all(files.map(f => $.fs.read(`${dir}/${f.name}`).catch(() => '')))
152      const me = await $.session.id().catch(() => '')
153      rows = texts
154        .map(parse)
155        .filter((r): r is Held => r !== null)
156        .map(r => ({ ...r, mine: r.session !== '' && r.session === me }))
157      rows.sort((a, b) => a.ts - b.ts)
158    }
159  } catch {
160    // No .reins/pending here, or it is unreadable: nothing to show.
161  }
162  await update($, held, () => rows)
163  // Claude Code puts the mod's name in front of this, so it does not say "reins" again.
164  $.ui.status(rows.length > 0 ? `${rows.length} held · /reins` : undefined)
165
166  const fresh = known === null ? [] : rows.filter(r => !known?.has(r.id))
167  known = new Set(rows.map(r => r.id))
168  const first = fresh[0]
169  if (first !== undefined) {
170    const rest = fresh.length > 1 ? ` (+${fresh.length - 1} more)` : ''
171    $.ui.toast(`reins: held ${first.tool} [${first.rule}] ${first.what}${rest} · /reins to read it`, { timeoutMs: 8000 })
172  }
173}
174
175export const register: Register = on => {
176  on('session.start', async ($, e, next) => {
177    // Without the command there is no pane. The count and the line above the prompt still work.
178    await $.command
179      .register({ name: 'reins', description: 'Show the reins hold queue in a pane, with each full input' })
180      .catch(() => undefined)
181    await refresh($)
182    $.clock.every(REFRESH_MS, () => refresh($))
183
184    return next(e)
185  })
186
187  // A hold parks during a turn, at a tool call. Reading again here shows it
188  // without waiting for the timer.
189  on('turn.complete', async ($, e, next) => {
190    await refresh($)
191
192    return next(e)
193  })
194
195  // The pane opens only when asked for. Opened unasked on the main screen it
196  // would sit above the prompt and take the place of the conversation.
197  on('command.run', { command: 'reins' }, async $ => {
198    await refresh($)
199    const rows = await read($, held)
200    const opened = await $.ui.open({ id: PANE, title: PANE_TITLE })
201    const count = rows.length === 0 ? 'Nothing is held.' : `${rows.length} held.`
202
203    return { text: opened.isPlaced ? `${count} The reins pane is open.` : `${count} The reins pane could not be shown here: reins pending` }
204  })
205
206  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
207    const { Box, Text } = $.ui.resolve(e)
208    const rows = await read($, held)
209
210    return (
211      <Box flexDirection="column">
212        {rows.length === 0 && <Text dimColor>Nothing is held.</Text>}
213        {rows.map(r => (
214          <Box key={r.id} flexDirection="column" marginBottom={1}>
215            <Text bold>
216              {r.id} · {r.tool}
217              {clock(r.ts) === '' ? '' : ` · parked ${clock(r.ts)}`}
218              {r.mine ? ' · this session' : ''}
219            </Text>
220            <Text dimColor>
221              [{r.rule}] {r.reason}
222            </Text>
223            {r.cwd !== '' && <Text dimColor>in {r.cwd}</Text>}
224            {r.lines.map((line, i) => (
225              <Text key={`${r.id}-${i}`}>{line === '' ? ' ' : line}</Text>
226            ))}
227            {r.cut > 0 && <Text dimColor>… {r.cut} more lines. The whole input is in reins watch.</Text>}
228            <Text dimColor>
229              reins approve {r.id} · reins deny {r.id}
230            </Text>
231          </Box>
232        ))}
233        {rows.length > 0 && <Text dimColor>Answered in a terminal. This pane shows the queue and cannot answer it.</Text>}
234      </Box>
235    )
236  })
237
238  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
239    const rows = await read($, held)
240    if (e.props.hasSurvey || rows.length === 0) {
241      return next(e)
242    }
243    const { Box, Text } = $.ui.resolve(e)
244    const first = rows[0]
245
246    return (
247      <Box flexDirection="column">
248        <Text bold>
249          reins: {rows.length} held, waiting on you
250        </Text>
251        <Text dimColor>
252          {first?.id} {first?.tool} [{first?.rule}] {first?.what}
253          {rows.length > 1 ? ` (+${rows.length - 1} more)` : ''} · /reins to read {rows.length > 1 ? 'them' : 'it'}
254        </Text>
255      </Box>
256    )
257  })
258}
259
types/index.d.ts 8 lines
1export type Held = { id: string; session: string; tool: string; rule: string; what: string; ts: number; reason: string; cwd: string; lines: string[]; cut: number; mine?: boolean }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'reins-status': { held: readonly Held[] }
6  }
7}
8