SLOPSHOPPER

shot

/shot: a screenshot straight into the prompt — a region, the window behind the terminal, the whole screen or the clipboard / /shot: скриншот прямо в промпт —…

newcommandpromptprocess
★ 1v1.0.0MITupdated 2026-10-26ArtyaEXE/claude-mods/shot
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · shot
› 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 › /shot ⎿ shot: Screenshots work on Windows only. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

shot

/shot: a screenshot straight into the prompt — a region, the window behind the terminal, the whole screen or the clipboard.

English · Русский

English

Showing Claude what you see on Windows usually takes a snip, a save and a path to type. /shot does it in one go: it saves a PNG and puts @<path> into the prompt at the cursor; when you send the message, Claude opens the picture.

  • /shot: the Windows snipping overlay opens (the same as Win+Shift+S); select a region, and it lands in the prompt.
  • /shot window: the window that was in front before you switched to the terminal (a browser with a bug, an app), captured whole even where the terminal covers it.
  • /shot screen: every monitor.
  • /shot clip: the image already on the clipboard.

Several screenshots in one message: a slash command only works in an empty prompt, so use the 📷 shot · window buttons the hud mod shows when this mod is installed: each picture lands at the cursor and what you typed stays.

Screenshots are saved to %TEMP%\claude-shots\shot-<date>-<time>.png. Claude Code does not attach a picture for an @ mention by itself, so when a prompt names one of these files the mod tells Claude to open it.

What it touches

On /shot runs one PowerShell script that takes the picture (the snipping overlay through ms-screenclip: and the clipboard, PrintWindow for a window, CopyFromScreen for the screen; /shot clears the clipboard before the overlay opens) and saves it under %TEMP%\claude-shots. Puts the file's @mention into the prompt box; when a sent prompt names such a file, adds a note asking Claude to open it. Adds the /shot command. Windows only.

Settings (/config)

  • language: auto (default; follows LANG, else the Windows UI language or regional format), ru or en.

Install

/plugin install shot --marketplace ArtyaEXE/claude-mods

Part of claude-mods. MIT license.


Русский

/shot: скриншот прямо в промпт — область, окно за терминалом, весь экран или буфер обмена.

Чтобы показать Claude то, что вы видите, на Windows обычно нужно сделать снимок, сохранить его и вписать путь. /shot делает это за раз: сохраняет PNG и вставляет @<путь> в промпт у курсора; когда вы отправите сообщение, Claude откроет картинку.

  • /shot: открывается выделение области Windows (то же, что Win+Shift+S); выделили — картинка в промпте.
  • /shot window: окно, которое было на переднем плане до того, как вы переключились в терминал (браузер с багом, приложение); снимается целиком, даже там, где его перекрывает терминал.
  • /shot screen: все мониторы.
  • /shot clip: картинка, которая уже лежит в буфере обмена.

Несколько скриншотов в одном сообщении: слэш-команда работает только в пустом поле ввода, поэтому пользуйтесь кнопками 📷 скрин · окно, которые показывает мод hud, когда установлен этот мод: каждая картинка встаёт у курсора, набранный текст остаётся.

Скриншоты сохраняются в %TEMP%\claude-shots\shot-<дата>-<время>.png. Claude Code сам не прикладывает картинку по @-упоминанию, поэтому, когда в промпте есть путь к такому файлу, мод просит Claude открыть его.

Что мод делает в системе

По /shot запускает один PowerShell-скрипт, который делает снимок (выделение области через ms-screenclip: и буфер обмена, PrintWindow для окна, CopyFromScreen для экрана; перед выделением области /shot очищает буфер обмена) и сохраняет его в %TEMP%\claude-shots. Вставляет @упоминание файла в поле ввода; если в отправленном промпте есть путь к такому файлу, добавляет для Claude просьбу открыть его. Добавляет команду /shot. Только Windows.

Настройки (/config)

  • language: auto (по умолчанию; по LANG, иначе по языку интерфейса или региональному формату Windows), ru или en.

Установка

/plugin install shot --marketplace ArtyaEXE/claude-mods

Часть claude-mods. Лицензия MIT.

Source 1 files
hooks/register.ts 228 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3type Lang = 'ru' | 'en'
4
5// `ru` / `en` as set; `auto`: the locale variables first, then Windows' UI language or regional format.
6async function resolveLang($: EngineInterface, setting: unknown): Promise<Lang> {
7  if (setting === 'ru' || setting === 'en') return setting
8  const env = (await $.env.get('LC_ALL')) || (await $.env.get('LC_MESSAGES')) || (await $.env.get('LANG'))
9  if (env) return env.toLowerCase().startsWith('ru') ? 'ru' : 'en'
10  try {
11    const ran = await $.process.run(
12      ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', '(Get-UICulture).Name; (Get-Culture).Name'],
13      { timeoutMs: 10_000 },
14    )
15    if (ran.exitCode === 0 && /^ru\b/im.test(ran.stdout)) return 'ru'
16  } catch {
17    // Not Windows: English.
18  }
19
20  return 'en'
21}
22
23type Mode = 'select' | 'window' | 'screen' | 'clip'
24
25// Saves a picture to SHOT_PATH as PNG, by SHOT_MODE: select (the Windows snipping overlay, then the clipboard),
26// window (the window behind the terminal, drawn by itself even where the terminal covers it), screen (every
27// monitor), clip (the image on the clipboard). Prints `ok <width>x<height> <program>`, or the error.
28const SHOT = String.raw`
29$ProgressPreference = 'SilentlyContinue'
30trap { [Console]::Out.WriteLine('error: ' + $_.Exception.Message); exit 1 }
31Add-Type -AssemblyName System.Windows.Forms, System.Drawing
32Add-Type -Namespace Shot -Name Win32 -MemberDefinition @'
33[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
34[DllImport("user32.dll")] public static extern IntPtr GetWindow(IntPtr h, uint cmd);
35[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
36[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
37[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr h);
38[DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i);
39[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
40[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr dc, uint flags);
41[DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int size);
42[DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr h, int a, out int v, int size);
43[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
44[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
45[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
46'@
47[void][Shot.Win32]::SetProcessDPIAware()
48$mode = $env:SHOT_MODE
49$out = $env:SHOT_PATH
50$bmp = $null
51$from = ''
52
53if ($mode -eq 'window') {
54  # The first real window under the foreground one (the terminal) in z-order: what was in front before it.
55  $fg = [Shot.Win32]::GetForegroundWindow()
56  $fgPid = 0; [void][Shot.Win32]::GetWindowThreadProcessId($fg, [ref]$fgPid)
57  $h = $fg
58  $target = [IntPtr]::Zero
59  $targetPid = 0
60  for ($i = 0; $i -lt 500; $i++) {
61    $h = [Shot.Win32]::GetWindow($h, 2)
62    if ($h -eq [IntPtr]::Zero) { break }
63    if (-not [Shot.Win32]::IsWindowVisible($h) -or [Shot.Win32]::IsIconic($h)) { continue }
64    if ([Shot.Win32]::GetWindowTextLength($h) -eq 0) { continue }
65    if ([Shot.Win32]::GetWindowLong($h, -20) -band 0x80) { continue }
66    $cloaked = 0; [void][Shot.Win32]::DwmGetWindowAttribute($h, 14, [ref]$cloaked, 4)
67    if ($cloaked -ne 0) { continue }
68    $p = 0; [void][Shot.Win32]::GetWindowThreadProcessId($h, [ref]$p)
69    if ($p -eq $fgPid) { continue }
70    $target = $h; $targetPid = $p; break
71  }
72  if ($target -eq [IntPtr]::Zero) { throw 'no window behind the terminal' }
73  $from = (Get-Process -Id $targetPid -ErrorAction SilentlyContinue).ProcessName
74  $r = New-Object Shot.Win32+RECT
75  [void][Shot.Win32]::GetWindowRect($target, [ref]$r)
76  $w = $r.Right - $r.Left; $hgt = $r.Bottom - $r.Top
77  $bmp = New-Object Drawing.Bitmap $w, $hgt
78  $g = [Drawing.Graphics]::FromImage($bmp)
79  $dc = $g.GetHdc()
80  # PW_RENDERFULLCONTENT: GPU-drawn windows (browsers, editors) too.
81  [void][Shot.Win32]::PrintWindow($target, $dc, 2)
82  $g.ReleaseHdc($dc); $g.Dispose()
83  # Trim the invisible resize border Windows 10 / 11 adds around a window.
84  $f = New-Object Shot.Win32+RECT
85  if ([Shot.Win32]::DwmGetWindowAttribute($target, 9, [ref]$f, 16) -eq 0) {
86    $crop = New-Object Drawing.Rectangle ($f.Left - $r.Left), ($f.Top - $r.Top), ($f.Right - $f.Left), ($f.Bottom - $f.Top)
87    if ($crop.Width -gt 0 -and $crop.Height -gt 0 -and $crop.Right -le $w -and $crop.Bottom -le $hgt) { $bmp = $bmp.Clone($crop, $bmp.PixelFormat) }
88  }
89} elseif ($mode -eq 'screen') {
90  $v = [Windows.Forms.SystemInformation]::VirtualScreen
91  $bmp = New-Object Drawing.Bitmap $v.Width, $v.Height
92  $g = [Drawing.Graphics]::FromImage($bmp)
93  $g.CopyFromScreen($v.Left, $v.Top, 0, 0, $bmp.Size); $g.Dispose()
94} else {
95  if ($mode -eq 'select') {
96    [Windows.Forms.Clipboard]::Clear()
97    Start-Process 'ms-screenclip:'
98    $deadline = (Get-Date).AddSeconds(90)
99    while ((Get-Date) -lt $deadline -and -not [Windows.Forms.Clipboard]::ContainsImage()) { Start-Sleep -Milliseconds 300 }
100    if (-not [Windows.Forms.Clipboard]::ContainsImage()) { throw 'nothing was selected' }
101  }
102  if (-not [Windows.Forms.Clipboard]::ContainsImage()) { throw 'no image on the clipboard' }
103  $bmp = [Windows.Forms.Clipboard]::GetImage()
104}
105[void](New-Item -ItemType Directory -Force (Split-Path $out))
106$bmp.Save($out, [Drawing.Imaging.ImageFormat]::Png)
107[Console]::Out.WriteLine('ok ' + $bmp.Width + 'x' + $bmp.Height + ' ' + $from)
108`
109
110const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
111
112// `-EncodedCommand` takes the script as base64 UTF-16LE: Windows' argv quoting cannot strip its quotes.
113function encodeCommand(script: string): string {
114  const bytes: number[] = []
115  for (let i = 0; i < script.length; i++) {
116    const c = script.charCodeAt(i)
117    bytes.push(c & 0xff, c >> 8)
118  }
119  let out = ''
120  for (let i = 0; i < bytes.length; i += 3) {
121    const n = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0)
122    out += B64.charAt((n >> 18) & 63) + B64.charAt((n >> 12) & 63)
123    out += i + 1 < bytes.length ? B64.charAt((n >> 6) & 63) : '='
124    out += i + 2 < bytes.length ? B64.charAt(n & 63) : '='
125  }
126
127  return out
128}
129
130const STRINGS = {
131  ru: {
132    command: 'Скриншот в промпт: область (по умолчанию), window — окно за терминалом, screen — весь экран, clip — из буфера',
133    added: (size: string, from: string, mention: string) => `Скриншот ${size}${from ? ` (${from})` : ''} добавлен в промпт: ${mention}`,
134    copyIt: (mention: string) => `Скриншот сохранён, вставьте в промпт: ${mention}`,
135    errors: {
136      'no window behind the terminal': 'За терминалом нет окна, которое можно снять.',
137      'nothing was selected': 'Область не выделена: скриншот отменён.',
138      'no image on the clipboard': 'В буфере обмена нет картинки.',
139    } as Record<string, string>,
140    failed: (why: string) => `Не получилось сделать скриншот: ${why}`,
141    notWindows: 'Скриншоты работают только на Windows.',
142    unknownMode: (m: string) => `Не знаю режим «${m}». Есть: window, screen, clip или пусто для выделения области.`,
143  },
144  en: {
145    command: 'Screenshot into the prompt: a region (default), window: the window behind the terminal, screen, clip: from the clipboard',
146    added: (size: string, from: string, mention: string) => `Screenshot ${size}${from ? ` (${from})` : ''} added to the prompt: ${mention}`,
147    copyIt: (mention: string) => `Screenshot saved; put it in the prompt: ${mention}`,
148    errors: {
149      'no window behind the terminal': 'There is no window behind the terminal to capture.',
150      'nothing was selected': 'No region was selected: screenshot cancelled.',
151      'no image on the clipboard': 'There is no image on the clipboard.',
152    } as Record<string, string>,
153    failed: (why: string) => `Could not take the screenshot: ${why}`,
154    notWindows: 'Screenshots work on Windows only.',
155    unknownMode: (m: string) => `Unknown mode "${m}". There are: window, screen, clip, or nothing to select a region.`,
156  },
157}
158
159// A screenshot this mod saved, as the prompt names it (quoted or not).
160const SHOT_PATH = /@?"?([A-Za-z]:[\\/][^"\n]*?[\\/]claude-shots[\\/]shot-\d{8}-\d{6}\.png)"?/g
161
162const FOR_MODEL = (paths: string[]) =>
163  `The user attached ${paths.length === 1 ? 'a screenshot' : 'screenshots'} with /shot: ${paths.join(', ')}. ` +
164  'Unless the picture is already attached above, open each file with the Read tool to see it before answering.'
165
166let lang: Lang = 'en'
167
168const stamp = () => {
169  const d = new Date()
170  const two = (n: number) => String(n).padStart(2, '0')
171
172  return `${d.getFullYear()}${two(d.getMonth() + 1)}${two(d.getDate())}-${two(d.getHours())}${two(d.getMinutes())}${two(d.getSeconds())}`
173}
174
175// Takes the picture; the text the command answers with.
176async function shoot($: EngineInterface, mode: Mode): Promise<string> {
177  const L = STRINGS[lang]
178  const temp = (await $.env.get('TEMP')) ?? (await $.env.get('TMP'))
179  if ((await $.env.get('OS')) !== 'Windows_NT' || temp === undefined) return L.notWindows
180  const path = `${temp.replace(/[\\/]$/, '')}\\claude-shots\\shot-${stamp()}.png`
181  let line = ''
182  try {
183    const ran = await $.process.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-STA', '-EncodedCommand', encodeCommand(SHOT)], {
184      env: { SHOT_MODE: mode, SHOT_PATH: path },
185      timeoutMs: 120_000,
186    })
187    line = ran.stdout.trim().split('\n').pop()?.trim() ?? ''
188  } catch (error) {
189    return L.failed(String(error))
190  }
191  const ok = /^ok (\d+x\d+) ?(.*)$/.exec(line)
192  if (ok === null) {
193    const why = line.replace(/^error: /, '')
194
195    return L.errors[why] ?? L.failed(why || 'PowerShell')
196  }
197  const mention = /\s/.test(path) ? `@"${path}"` : `@${path}`
198  const filled = await $.prompt.fill({ text: `${mention} `, mode: 'insert' })
199
200  return filled.isFilled ? L.added(ok[1] ?? '', ok[2] ?? '', mention) : L.copyIt(mention)
201}
202
203export const register: Register = (on, options) => {
204  on('session.start', async ($, e, next) => {
205    const started = await next(e)
206    lang = await resolveLang($, (options as { language?: unknown }).language)
207    await $.command.register({ name: 'shot', description: STRINGS[lang].command, argumentHint: 'window | screen | clip' })
208
209    return started
210  })
211
212  // An @mention of a screenshot is not attached as a picture by Claude Code: tell the model to open each one itself.
213  on('prompt.submit', async ($, e, next) => {
214    const shots = [...new Set([...e.text.matchAll(SHOT_PATH)].map(m => m[1] ?? ''))].filter(p => p !== '')
215    if (shots.length === 0) return next(e)
216
217    return next({ ...e, context: [...(e.context ?? []), FOR_MODEL(shots)] })
218  })
219
220  on('command.run', { command: 'shot' }, async ($, e) => {
221    const arg = e.args.trim().toLowerCase()
222    const mode: Mode | undefined = arg === '' ? 'select' : arg === 'window' || arg === 'screen' || arg === 'clip' ? arg : undefined
223    if (mode === undefined) return { text: STRINGS[lang].unknownMode(arg) }
224
225    return { text: await shoot($, mode) }
226  })
227}
228