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

/shot: a screenshot straight into the prompt — a region, the window behind the terminal, the whole screen or the clipboard.
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.
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.
/config)language: auto (default; follows LANG, else the Windows UI language or regional format), ru or en./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.
hooks/register.ts 228 lines1import 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