SLOPSHOPPER

Valet Mode

A look-only mode for someone else at your PC: Claude can read and answer, but cannot change anything until you unlock it.

bandguardcommandtimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · valet-mode
› 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 › /valet ⎿ valet-mode: Valet Mode is off. /valet on <code> turns it on. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Valet Mode

A look-only mode for when someone else is at your PC. Like the valet key for a car: they can drive Claude's questions and answers, but they cannot open the trunk.

  • /valet on 4821 turns it on with a code you pick, 4 characters or more.
  • /valet off 4821 turns it off. The wrong code leaves it on.
  • /valet says whether it is on.

While it is on, Claude can still read files, search and answer questions. It cannot edit or write files, run shell commands, or use connectors that act for you, such as sending email. A yellow VALET MODE band sits above the prompt so nobody forgets it is on.

It locks every Claude Code window on the PC at once, including windows that were already open, and the band shows up in each of them within a couple of seconds. It stays on through a restart, so closing Claude Code does not unlock it. The lock and its code are kept in one small file on your own machine, .claude/rostech/valet-mode.json in your user folder. If that file is damaged, Valet Mode stays locked rather than opening up. It is a simple lock for a guest at your desk, not a security system. It sends nothing anywhere.

Tested with Claude Code 2.1.288.

More mods

See every Rostech mod, live, at rostech.app.

Source 2 files
hooks/register.tsx 104 lines
1import { atom, read, update } from 'claude-code'
2import type { Engine, Register } from 'claude-code'
3
4// Valet Mode: someone else is at your PC. Claude can look and talk, never touch.
5// The lock lives in one file every Claude Code window reads, so turning it on in
6// one window locks them all, and a restart does not unlock it.
7const isOn = atom({ plugin: 'valet-mode', key: 'isOn' } as const, false)
8
9// While Valet Mode is on, only tools that read are let through. Everything else,
10// from file edits and shell commands to connectors that send mail, is refused.
11const READ_ONLY = new Set(['Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'])
12
13// How often an open window looks at the file, so its band appears and goes
14// within a couple of seconds of another window changing the lock.
15const LOOK_EVERY_MS = 2000
16
17type Lock = { isOn: boolean; code: string }
18
19const lockFile = async ($: Engine) => {
20  const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')) ?? '.'
21  return `${home.split(String.fromCharCode(92)).join('/')}/.claude/rostech/valet-mode.json`
22}
23
24const readLock = async ($: Engine): Promise<Lock> => {
25  const file = await lockFile($)
26  if (!(await $.fs.exists(file))) return { isOn: false, code: '' }
27  try {
28    const kept = JSON.parse(String(await $.fs.read(file))) as Partial<Lock>
29    return { isOn: kept.isOn === true, code: String(kept.code ?? '') }
30  } catch {
31    // A lock file nobody can read counts as locked: a guest must not get the
32    // keys because the file was damaged.
33    return { isOn: true, code: '' }
34  }
35}
36
37const writeLock = async ($: Engine, lock: Lock) => {
38  await $.fs.write(await lockFile($), JSON.stringify(lock))
39  await update($, isOn, () => lock.isOn)
40}
41
42export const register: Register = on => {
43  on('session.start', async ($, e, next) => {
44    await $.command.register({ name: 'valet', description: 'Lock Claude to look-only for someone else at your PC. /valet on <code>, /valet off <code>.' })
45    // 0.1.0 kept the lock in one window's plugin store. Carry a lock that is
46    // still on into the shared file, so the update never unlocks anything.
47    if ((await $.store.get('isOn')) === true) {
48      const code = String((await $.store.get('code')) ?? '')
49      if (!(await readLock($)).isOn) await writeLock($, { isOn: true, code })
50      await $.store.set('isOn', false)
51    }
52    const sync = async () => {
53      const lock = await readLock($)
54      if (lock.isOn !== (await read($, isOn))) await update($, isOn, () => lock.isOn)
55    }
56    await sync()
57    $.clock.every(LOOK_EVERY_MS, () => void sync())
58    return next(e)
59  })
60
61  on('tool.call', async ($, e, next) => {
62    // The file is read on every call, never a copy, so a window opened before
63    // the lock went on is locked from its very next tool call.
64    if (!READ_ONLY.has(e.tool) && (await readLock($)).isOn) {
65      return { deny: `Valet Mode is on: ${e.tool} is locked. Claude can read and answer, but cannot change anything until the owner turns Valet Mode off.` }
66    }
67    return next(e)
68  })
69
70  on('command.run', { command: 'valet' }, async ($, e) => {
71    const [verb, ...rest] = e.args.trim().split(/\s+/)
72    const code = rest.join(' ')
73    const lock = await readLock($)
74    if (verb === 'on') {
75      if (lock.isOn) return { text: '🎩 Valet Mode is already on.' }
76      if (code.length < 4) return { text: 'Pick a code of 4 or more characters: /valet on 1234. You need it to turn Valet Mode off.' }
77      await writeLock($, { isOn: true, code })
78      return { text: '🎩 Valet Mode on, in every Claude Code window. Claude can look and answer, but cannot change anything. /valet off <your code> unlocks it.' }
79    }
80    if (verb === 'off') {
81      if (!lock.isOn) return { text: 'Valet Mode is already off.' }
82      if (code !== lock.code) return { text: 'Wrong code. Valet Mode stays on.' }
83      await writeLock($, { isOn: false, code: '' })
84      return { text: 'Valet Mode off, in every window. Claude has its hands back.' }
85    }
86    return { text: lock.isOn ? '🎩 Valet Mode is on. Claude can look and answer, but cannot change anything.' : 'Valet Mode is off. /valet on <code> turns it on.' }
87  })
88
89  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90    if (e.props.hasSurvey || !(await read($, isOn))) return next(e)
91    const { Box, Text } = $.ui.resolve(e)
92    const below = await next(e)
93    return (
94      <Box flexDirection="column">
95        <Box flexDirection="row">
96          <Text backgroundColor="yellow" color="black" bold> 🎩 VALET MODE </Text>
97          <Text>  Claude can look and answer. It cannot change anything.</Text>
98        </Box>
99        {below}
100      </Box>
101    )
102  })
103}
104
types/index.d.ts 6 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'valet-mode': { isOn: boolean }
4  }
5}
6