Choose the prompt-cache TTL (5 minutes or 1 hour) per Claude Code session.

A Claude Code plugin that lets you choose the prompt-cache TTL for each session.
Claude Code caches the conversation prefix between turns. With a 5-minute TTL the cache expires after a short break and the next turn rewrites the whole context at cache-write price. A 1-hour TTL survives longer breaks, but each cache write costs more (2× the input price instead of 1.25×). Which one is cheaper depends on how you work in that session.
1 hour and 5 minutes. The current default is listed first. Dismiss the question to keep the default./ttl 5m or /ttl 1h changes the TTL at any point in the session. /ttl alone shows the current value.TTL 5m or TTL 1h once a choice is made.The choice applies to the current session only. It sets CLAUDE_CODE_PROMPT_CACHE_TTL in that session's process, which takes precedence over the promptCacheTtl setting. Other sessions and your settings files are not changed.
Non-interactive runs (claude -p, SDK) are never asked anything.
/plugin marketplace add daanmeersseman/claude-workbench
/plugin install cache-ttl-picker@claude-workbench
Pick the user scope to make it active in every session.
Update with /plugin marketplace update claude-workbench, then /plugin update cache-ttl-picker and /reload-plugins.
The default shown first comes from, in order: the CLAUDE_CODE_PROMPT_CACHE_TTL environment variable, the promptCacheTtl setting, otherwise 5m. To make 1 hour the default, add this to your user settings.json:
{ "promptCacheTtl": "1h" }
CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL / subagentPromptCacheTtl, which this plugin leaves alone.claude plugin validate plugins/cache-ttl-picker
claude plugin test plugins/cache-ttl-picker
claude --plugin-dir plugins/cache-ttl-pickerhooks/register.tsx 50 lines1import type { Hook, Register } from 'claude-code'
2
3type Ttl = '5m' | '1h'
4type Api = Parameters<Hook<'session.start'>>[0]
5
6const LABELS: Record<Ttl, string> = { '1h': '1 hour', '5m': '5 minutes' }
7
8function parseTtl(text: string): Ttl | undefined {
9 const t = text.trim().toLowerCase()
10 if (t === '1h' || t === LABELS['1h']) return '1h'
11 if (t === '5m' || t === LABELS['5m'].toLowerCase()) return '5m'
12 return undefined
13}
14
15async function current($: Api): Promise<Ttl> {
16 const env = await $.env.get('CLAUDE_CODE_PROMPT_CACHE_TTL')
17 const settings = ((await $.settings.read().catch(() => ({}))) ?? {}) as Record<string, unknown>
18 return (env ?? settings.promptCacheTtl) === '1h' ? '1h' : '5m'
19}
20
21async function apply($: Api, ttl: Ttl): Promise<void> {
22 await $.env.set('CLAUDE_CODE_PROMPT_CACHE_TTL', ttl)
23 $.ui.status(`TTL ${ttl}`)
24}
25
26async function askTtl($: Api): Promise<void> {
27 const def = await current($)
28 const options = def === '1h' ? [LABELS['1h'], LABELS['5m']] : [LABELS['5m'], LABELS['1h']]
29 const answer = await $.ui.ask('Prompt-cache TTL for this session?', { header: 'Cache TTL', options })
30 const ttl = parseTtl(answer)
31 if (ttl) await apply($, ttl)
32}
33
34export const register: Register = on => {
35 on('session.start', async ($, e, next) => {
36 const result = await next(e)
37 await $.command.register({ name: 'ttl', description: 'Prompt-cache TTL for this session: /ttl 5m|1h' })
38 // Not awaited, so the session start never waits on the dialog.
39 if (e.isInteractive) void askTtl($).catch(() => {})
40 return result
41 })
42
43 on('command.run', { command: 'ttl' }, async ($, e) => {
44 const ttl = parseTtl(e.args)
45 if (!ttl) return { text: `Prompt-cache TTL: ${await current($)} (use /ttl 5m or /ttl 1h)` }
46 await apply($, ttl)
47 return { text: `Prompt-cache TTL: ${ttl}` }
48 })
49}
50