SLOPSHOPPER

copy-commands

Holds the shell blocks Claude prints for you to run, and copies them on a keypress or /copy-commands

newbandcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · copy-commands
› 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(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /copy-commands ⎿ copy-commands: copy-commands: nothing waiting to copy ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-code-mods

Small mods for Claude Code.

cache-timer

Claude Code caches your conversation prefix between requests. While the cache is warm, the next request reads it cheaply; once it expires, the whole context is written again at the higher cache-write rate. cache-timer shows how long you have left.

cache ███████░░░░░ 35m

The bar is green with half or more of the time left, yellow from 20%, red below that, and reads cold once the cache has expired. Under two minutes the label counts seconds.

Which TTL it counts down

Each API response reports how many tokens it wrote to the cache at each TTL (ephemeral_1h_input_tokens, ephemeral_5m_input_tokens). cache-timer reads the newest main-thread response that wrote to the cache and uses that TTL, so it follows what Claude Code actually did: 1 hour on a subscription within its usage limits, 5 minutes on an API key, Bedrock, Vertex or Foundry, after a promptCacheTtl setting, or once a subscription goes into overage. Subagent requests keep their own cache and are ignored.

To force a TTL instead, set the plugin's ttl option to 5m or 1h (/config), and for the status line script set CACHE_TIMER_TTL=5m or 1h. Until a TTL is known it assumes CLAUDE_CODE_PROMPT_CACHE_TTL if set, otherwise 1 hour.

Status line

plugins/cache-timer/statusline/cache-bar.sh prints the bar for a Claude Code status line, in ANSI color. It needs jq and runs on macOS and Linux.

Use it on its own in ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "bash /path/to/claude-code-mods/plugins/cache-timer/statusline/cache-bar.sh",
    "refreshInterval": 1
  }
}

Or add it to a status line script you already have, which reads the same JSON on stdin:

input=$(cat)
cache=$(printf '%s' "$input" | bash /path/to/cache-bar.sh)

refreshInterval makes the countdown move while you are idle; without it the status line only redraws on activity.

Bar above the prompt (plugin)

The plugin draws the same bar in the row above the prompt while Claude is idle. It uses Claude Code's plugin hooks module API (hooks/register.tsx), so it needs a Claude Code version that has it.

claude plugin marketplace add ya8282/claude-code-mods
claude plugin install cache-timer@claude-code-mods

Or load a local clone for one session with claude --plugin-dir plugins/cache-timer.

Development

claude plugin validate plugins/cache-timer
claude plugin test plugins/cache-timer

copy-commands

When Claude ends a turn with commands for you to run (closing beads, git commands it cannot run itself), copy-commands holds them so you can paste them into another tmux pane or terminal.

It takes the fenced blocks tagged sh, bash, zsh, shell or console, or untagged, from the main thread's final answer, and keeps them until you copy them or a later answer brings new ones. A line above the prompt shows how many command lines are waiting:

  • ctrl+x tab, then c copies them and clears the line; x dismisses them.
  • /copy-commands copies them too.

Each session holds its own, so several can finish before you paste. The copy goes through Claude Code's clipboard path (pbcopy, wl-copy, xclip, tmux's buffer, or OSC 52 over SSH).

claude plugin install copy-commands@claude-code-mods

Development: claude plugin validate plugins/copy-commands and claude plugin test plugins/copy-commands.

License

MIT

Source 2 files
hooks/register.tsx 70 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderSurface } from 'claude-code'
3
4const SHELL = new Set(['', 'sh', 'bash', 'zsh', 'shell', 'console'])
5const FENCE = /^```([\w-]*)[^\n]*\n([\s\S]*?)^```[ \t]*$/gm
6
7const pending = atom({ plugin: 'copy-commands', key: 'pending' } as const, null)
8
9// Untagged blocks count too: printed commands are often fenced without a language.
10export const shellBlocks = (text: string): string[] =>
11  [...text.matchAll(FENCE)]
12    .filter(([, lang = '']) => SHELL.has(lang.toLowerCase()))
13    .map(([, , body = '']) => body)
14    .filter(body => body.trim() !== '')
15
16export const commandLines = (text: string): number =>
17  text.split('\n').filter(l => l.trim() && !l.trimStart().startsWith('#')).length
18
19const copyPending = async ($: EngineInterface, surface?: RenderSurface): Promise<string> => {
20  const text = await read($, pending)
21  if (text === null) return 'copy-commands: nothing waiting to copy'
22
23  const copied = await $.ui.copy({ text, surface })
24  if (!copied.isCopied) return `copy-commands: not copied (${copied.reason})`
25
26  await update($, pending, () => null)
27  return `Copied ${commandLines(text)} command line(s) to the clipboard`
28}
29
30export const register: Register = on => {
31  on('session.start', async ($, e, next) => {
32    await $.command.register({ name: 'copy-commands', description: 'Copy the commands Claude last printed for you to run' })
33    return next(e)
34  })
35
36  on('command.run', { command: 'copy-commands' }, async $ => ({ text: await copyPending($) }))
37
38  // Held until copied or replaced, so a session that finishes while you are elsewhere keeps them.
39  on('turn.complete', async ($, e, next) => {
40    const result = await next(e)
41    if (e.agentId !== undefined || e.reason !== 'answer') return result
42
43    const blocks = shellBlocks(e.answer)
44    if (blocks.length > 0) await update($, pending, () => blocks.join('\n'))
45
46    return result
47  })
48
49  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
50    const text = await read($, pending)
51    if (text === null || e.props.hasSurvey) return next(e)
52
53    const { Box, Button, Text } = $.ui.resolve(e)
54
55    return (
56      <Box>
57        <Text color="yellow">{commandLines(text)} command line(s) ready </Text>
58        <Button key="copy" hotkey="c" plain onPress={async press => $.ui.toast(await copyPending($, press.surface))}>
59          Copy
60        </Button>
61        <Text> </Text>
62        <Button key="dismiss" hotkey="x" plain onPress={() => update($, pending, () => null)}>
63          Dismiss
64        </Button>
65        <Text dimColor>  (ctrl+x tab, then c)</Text>
66      </Box>
67    )
68  })
69}
70
types/index.d.ts 8 lines
1export type Pending = string | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    'copy-commands': { pending: Pending }
6  }
7}
8