SLOPSHOPPER

webfetch-cache

Short-circuits repeated WebFetch calls for the same URL and prompt within a session, answering { result } from cache without a network round trip. A tool.call…

newguardcommand
v0.2.0MITupdated 2026-10-19Covre81/claude-code-mods/webfetch-cache
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · webfetch-cache
› 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 › /webfetch-cache ⎿ webfetch-cache: webfetch-cache esta ativado. Uso: /webfetch-cache on · /webfetch-cache off ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

webfetch-cache

Short-circuits repeated WebFetch calls for the same URL + prompt within a session. On a cache hit the hook answers { result } itself without calling next (no network call); on a miss it awaits the real fetch, stores the tool's record and returns what came back.

Only the tool's own record (result) is cached, never core's ref: that number names the messages core produced for one specific call and must not be replayed on another.

A long page read in pieces (WebFetch's offset, Claude Code 2.1.290+) is cached piece by piece: a read further down the page never gets the first page back.

/webfetch-cache on|off toggles it for the session (so meus-mods can control it too); turning it off also clears the in-memory cache.

Options

  ttlSeconds: number  how long an entry stays fresh (default 900)
  maxEntries: number  cache size (default 200)

Declared in .claude-plugin/plugin.json (userConfig). Set them in /config, in user settings (~/.claude/settings.json, not project settings), with --settings <file> or in managed settings:

{ "pluginConfigs": { "webfetch-cache@skills-dir": { "options": { } } } }

Install

npx claude-code-templates@latest --mod productivity/webfetch-cache
claude

It is written to .claude/skills/webfetch-cache/, which Claude Code auto-loads as webfetch-cache@skills-dir. For one session with hot reload: claude --plugin-dir .claude/skills/webfetch-cache. claude plugin validate .claude/skills/webfetch-cache prints every event it hooks and every $ call it makes.

Requirements. Mods are on by default in Claude Code 2.1.287+. Typed against Anthropic's declarations: https://github.com/anthropics/claude-code/tree/main/mods


Vendored from davila7/claude-code-templates (cli-tool/components/mods/productivity/webfetch-cache), MIT licensed. Added the isEnabled toggle above so it fits the on/off convention of every other mod in this repo.

Source 2 files
hooks/register.ts 74 lines
1// Vendored from davila7/claude-code-templates (cli-tool/components/mods/productivity/webfetch-cache),
2// MIT licensed. Added: an isEnabled toggle (/webfetch-cache on|off) so meus-mods can control it.
3import { atom, read, update } from 'claude-code'
4import type { Register, ToolCallResult } from 'claude-code'
5
6const isEnabledRef = atom({ plugin: 'webfetch-cache', key: 'isEnabled' } as const, true)
7
8type WebFetchRecord = Extract<ToolCallResult<'WebFetch'>, { result: unknown }>['result']
9
10interface Entry { result: WebFetchRecord; storedAt: number }
11
12// Module state lives for the session (a plugin's module is one worker).
13const cache = new Map<string, Entry>()
14
15export const register: Register = (on, options) => {
16  const ttlMs = (typeof options.ttlSeconds === 'number' ? options.ttlSeconds : 900) * 1000
17  const maxEntries = typeof options.maxEntries === 'number' ? options.maxEntries : 200
18
19  on('session.start', async ($, e, next) => {
20    const saved = await $.store.get('isEnabled').catch(() => undefined)
21    await update($, isEnabledRef, () => (typeof saved === 'boolean' ? saved : isEnabledRef.initial))
22    await $.command.register({
23      name: 'webfetch-cache',
24      description: 'Liga/desliga o cache de WebFetch repetido na mesma sessão.',
25      argumentHint: 'on|off',
26    })
27    return next(e)
28  })
29
30  on('command.run', { command: 'webfetch-cache' }, async ($, e) => {
31    const arg = e.args.trim().toLowerCase()
32    if (arg !== 'on' && arg !== 'off') {
33      const isEnabled = await read($, isEnabledRef)
34      return { text: `webfetch-cache esta ${isEnabled ? 'ativado' : 'desativado'}. Uso: /webfetch-cache on · /webfetch-cache off` }
35    }
36    await update($, isEnabledRef, () => arg === 'on')
37    await $.store.set('isEnabled', arg === 'on') // $.state is per session; the store survives a restart
38    if (arg === 'off') cache.clear()
39    return { text: `webfetch-cache ${arg === 'on' ? 'ativado' : 'desativado'}.` }
40  })
41
42  on('tool.call', { tool: 'WebFetch' }, async ($, e, next) => {
43    if (!e.url || !(await read($, isEnabledRef))) return next(e)
44    // WebFetch reads a long page in pieces (`offset`, Claude Code 2.1.290+): each piece is its own entry,
45    // or a read past the first page would get the first page back. Not in the typings until they are regenerated (TODO: drop the cast then).
46    const offset = (e as { offset?: unknown }).offset
47    const key = `${e.url}\n${e.prompt}\n${typeof offset === 'number' ? offset : 0}`
48    const now = Date.now()
49
50    const hit = cache.get(key)
51    if (hit && now - hit.storedAt < ttlMs) {
52      const age = Math.round((now - hit.storedAt) / 1000)
53      $.ui.log(`[webfetch-cache] hit for ${e.url} (${age}s old)`)
54      // Nothing below this hook runs: no network call.
55      return {
56        result: hit.result,
57        context: [`webfetch-cache served this result from a ${age}s-old cache entry for the same URL, prompt and offset.`],
58      }
59    }
60
61    const outcome = await next(e)
62
63    // Do not cache a denial or an errored fetch.
64    if (outcome.deny === undefined && !outcome.isError) {
65      if (cache.size >= maxEntries) {
66        const oldest = cache.keys().next().value
67        if (oldest !== undefined) cache.delete(oldest)
68      }
69      cache.set(key, { result: outcome.result, storedAt: now })
70    }
71    return outcome
72  })
73}
74
types/index.d.ts 12 lines
1export type WebfetchCacheState = {
2  isEnabled: boolean
3}
4
5declare module 'claude-code' {
6  interface PluginState {
7    'webfetch-cache': {
8      isEnabled: boolean
9    }
10  }
11}
12