SLOPSHOPPER

keep-awake

/keep-awake opens a visible Windows Terminal tab that keeps the laptop awake (12 hours by default); closing the tab ends it.

newspinnercommandprocesstimer
v0.1.0MITupdated 2026-10-25keyhearingaids/claude-keep-awake
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · keep-awake
› 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 › /keep-awake ⎿ keep-awake: Opening the Keep-awake tab in Windows Terminal. It has not reported in yet; run /keep-awake status in a moment ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

keep-awake

A Claude Code mod for Windows. /keep-awake opens a visible Windows Terminal tab named "Keep-awake" that stops the laptop from sleeping (and the screen from turning off) for 12 hours. Closing the tab ends it at once.

  • /keep-awake: start, 12 hours
  • /keep-awake 8: start for another number of hours (0.05 to 24)
  • /keep-awake status: on or off, and when it ends
  • /keep-awake stop: end it (same as closing the tab)

While it runs, the dim line under the prompt ends with "· awake until <day time>". Only one runs at a time. The tab keeps going if Claude Code is closed. Requires Windows Terminal (wt.exe) and PowerShell 7 (pwsh).

Install

At the prompt of a Claude Code terminal session:

/plugin install keep-awake --marketplace keyhearingaids/claude-keep-awake

Answer y to add the marketplace, then press Enter for the user scope (every project).

From a shell:

claude plugin install keep-awake --marketplace keyhearingaids/claude-keep-awake

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with keyhearingaids/claude-keep-awake, then Add plugin.

How it works

scripts/launch.ps1 opens scripts/keep-awake.ps1 in a new Windows Terminal tab. That script calls SetThreadExecutionState in a loop and writes its process id and end time to %LOCALAPPDATA%\claude-keep-awake\status.json, which the mod reads to show status. Windows drops the keep-awake request as soon as the tab's PowerShell process exits.

License

MIT. See LICENSE.

Source 2 files
hooks/register.ts 153 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Awake } from '../types'
5
6const DEFAULT_HOURS = 12
7// 0.05 h is 3 minutes: short enough to test with.
8const MIN_HOURS = 0.05
9const MAX_HOURS = 24
10// Named in full: the engine starts commands with a trimmed PATH, which the
11// new Terminal tab inherits.
12const PWSH = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
13
14const awake = atom({ plugin: 'keep-awake', key: 'awake' } as const, null as Awake | null)
15
16async function stateFile($: EngineInterface): Promise<string> {
17  const base = (await $.env.get('LOCALAPPDATA')) ?? (await $.env.get('TEMP')) ?? '.'
18
19  return `${base}\\claude-keep-awake\\status.json`
20}
21
22// The running keep-awake, or null: the status file the script wrote, its end
23// still ahead, and its process still alive (closing the tab kills it without
24// the script getting to delete the file).
25async function check($: EngineInterface): Promise<Awake | null> {
26  const path = await stateFile($)
27  if (!(await $.fs.exists(path))) {
28    return null
29  }
30
31  let found: Awake
32  try {
33    found = JSON.parse((await $.fs.read(path)).replace(/^/, '')) as Awake
34  } catch {
35    return null
36  }
37  if (!Number.isInteger(found.pid) || found.endsAt <= (await $.clock.now())) {
38    return null
39  }
40
41  const { stdout } = await $.process.run(
42    ['tasklist', '/FI', `PID eq ${found.pid}`, '/FO', 'CSV', '/NH'],
43    { timeoutMs: 10000 },
44  )
45
46  return stdout.includes(`"${found.pid}"`) ? found : null
47}
48
49async function refresh($: EngineInterface): Promise<Awake | null> {
50  const now = await check($).catch(() => null)
51  await update($, awake, () => now)
52
53  return now
54}
55
56async function start($: EngineInterface, hours: number): Promise<string> {
57  const running = await refresh($)
58  if (running) {
59    return `Keep-awake is already running, until ${running.label}. Close its tab (or /keep-awake stop) first to start a new one.`
60  }
61
62  // A command has 10 s in all; stop waiting for the tab well before that and
63  // let the minute timer catch a slower start.
64  const deadline = (await $.clock.now()) + 7500
65  const launcher = `${$.plugin.root}\\scripts\\launch.ps1`
66  const path = await stateFile($)
67  try {
68    const { exitCode, stderr } = await $.process.run(
69      [
70        PWSH, '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
71        '-File', launcher, '-Hours', String(hours), '-StateFile', path,
72      ],
73      { timeoutMs: 6000 },
74    )
75    if (exitCode !== 0 && stderr.trim()) {
76      return `Could not open Windows Terminal: ${stderr.trim()}`
77    }
78  } catch (error) {
79    return `Could not open Windows Terminal: ${String(error)}`
80  }
81
82  // The tab writes its status file once PowerShell is up.
83  while ((await $.clock.now()) < deadline) {
84    await $.clock.sleep(500)
85    const now = await refresh($)
86    if (now) {
87      return `Keep-awake started in a new Terminal tab, until ${now.label}. Close that tab to end it.`
88    }
89  }
90
91  return 'Opening the Keep-awake tab in Windows Terminal. It has not reported in yet; run /keep-awake status in a moment to confirm.'
92}
93
94async function stop($: EngineInterface): Promise<string> {
95  const running = await refresh($)
96  if (!running) {
97    return 'Keep-awake is not running.'
98  }
99
100  await $.process.run(['taskkill', '/PID', String(running.pid), '/F'], { timeoutMs: 10000 })
101  await refresh($)
102
103  return 'Keep-awake stopped. The laptop can sleep again.'
104}
105
106export const register: Register = on => {
107  on('session.start', async ($, e, next) => {
108    await $.command.register({
109      name: 'keep-awake',
110      description: 'Keep the laptop awake in a visible Terminal tab (12 hours unless you give a number of hours)',
111      argumentHint: '[hours | status | stop]',
112    })
113    void refresh($)
114    $.clock.every(60000, () => {
115      void refresh($)
116    })
117
118    return next(e)
119  })
120
121  on('command.run', { command: 'keep-awake' }, async ($, e) => {
122    const arg = e.args.trim().toLowerCase()
123
124    if (arg === 'stop') {
125      return { text: await stop($) }
126    }
127    if (arg === 'status') {
128      const now = await refresh($)
129
130      return { text: now ? `Keep-awake is on, until ${now.label}.` : 'Keep-awake is off.' }
131    }
132
133    const hours = arg === '' ? DEFAULT_HOURS : Number(arg.replace(/h(ours?)?$/, ''))
134    if (!Number.isFinite(hours) || hours < MIN_HOURS || hours > MAX_HOURS) {
135      return {
136        text: `Usage: /keep-awake (12 hours), /keep-awake <hours> (${MIN_HOURS} to ${MAX_HOURS}), /keep-awake status, /keep-awake stop.`,
137      }
138    }
139
140    return { text: await start($, hours) }
141  })
142
143  // The dim line under the prompt: add the end time while it runs.
144  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
145    const now = await read($, awake)
146    if (!now) {
147      return next(e)
148    }
149
150    return next({ ...e, props: { ...e.props, tail: ` · awake until ${now.label}` } })
151  })
152}
153
types/index.d.ts 10 lines
1// What the keep-awake script writes on start: its process id, when it ends
2// (Unix ms) and that time as the tab shows it ("Sat 2:14 AM").
3export type Awake = { pid: number; endsAt: number; label: string }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'keep-awake': { awake: Awake | null }
8  }
9}
10