A Search dropdown above the prompt: Native (WebSearch/WebFetch) or Exa / Firecrawl. Claude is told which to use, and the other side's tools are blocked.

My Claude Code setup as one plugin marketplace: mods, skills and an output style, installable on any machine.
claude plugin marketplace add FishyWeed/Claude-Toolkit
claude plugin install toolkit-setup@claude-toolkit
Then, inside Claude Code, run:
/toolkit-setup
It checks the machine, installs what you ask for, and lists every login and API key still needed. It never asks for key values in chat.
To install everything in one go:
claude plugin install prompt-enhancer@claude-toolkit
claude plugin install model-router@claude-toolkit
claude plugin install search-switch@claude-toolkit
claude plugin install mods-skills-panel@claude-toolkit
claude plugin install clear-caveman@claude-toolkit
claude plugin install design-directions@claude-toolkit
claude plugin install swarm@claude-toolkit
claude plugin install codeburn@claude-toolkit
This repo is private, so the machine must be signed in to GitHub (gh auth login, or Git's browser sign-in).
See TOOLKIT.md for every plugin: what it does, setup, and an example.
claude plugin marketplace update claude-toolkit
claude plugin update <plugin>@claude-toolkit
See CLAUDE.md. Short version: no secrets in the repo, keep TOOLKIT.md current, validate and test before committing.
hooks/register.tsx 96 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SearchMode } from '../types'
5
6const mode = atom({ plugin: 'search-switch', key: 'mode' } as const, 'native' as SearchMode)
7
8const OPTIONS = [
9 { value: 'native', label: 'Native' },
10 { value: 'exa', label: 'Exa / Firecrawl' },
11]
12
13const NATIVE_TOOLS = new Set(['WebSearch', 'WebFetch'])
14// Exa's MCP tools, whether it is installed as a plugin or as a plain MCP server.
15const isExaTool = (tool: string) => tool.startsWith('mcp__plugin_exa_') || tool.startsWith('mcp__exa__')
16const isFirecrawlCommand = (command: unknown) =>
17 typeof command === 'string' && /^\s*(npx\s+(-y\s+)?)?firecrawl(-cli)?\b/.test(command)
18
19const GUIDE: Record<SearchMode, string> = {
20 native:
21 'Web search mode: Native. For web search and fetching pages, use the built-in WebSearch and WebFetch tools. ' +
22 'Do not use the Exa MCP tools or the firecrawl CLI; they are blocked in this mode.',
23 exa:
24 'Web search mode: Exa / Firecrawl. Do not use the built-in WebSearch or WebFetch tools; they are blocked in this mode. ' +
25 'For web search, use the Exa MCP tools (web_search_exa, and web_fetch_exa to read a page). ' +
26 'For scraping, crawling or mapping a site, or when a page needs JavaScript rendering, use the firecrawl CLI through the Firecrawl skills. ' +
27 'If a tool fails because it is not signed in or is rate-limited, tell the user instead of falling back to WebSearch or WebFetch.',
28}
29
30const setMode = async ($: EngineInterface, next: SearchMode) => {
31 await update($, mode, () => next)
32 await $.store.set('mode', next)
33}
34
35const parseMode = (raw: string): SearchMode | undefined => {
36 const v = raw.trim().toLowerCase()
37 if (v === 'native') return 'native'
38 if (v === 'exa' || v === 'firecrawl' || v === 'exa/firecrawl') return 'exa'
39 return undefined
40}
41
42export const register: Register = on => {
43 on('session.start', async ($, e, next) => {
44 const saved = await $.store.get('mode').catch(() => undefined)
45 if (saved === 'native' || saved === 'exa') await update($, mode, () => saved)
46 await $.command.register({ name: 'search', description: 'Show or set the web search mode: native or exa' })
47 return next(e)
48 })
49
50 on('command.run', { command: 'search' }, async ($, e) => {
51 if (!e.args.trim()) return { text: `Search mode: ${(await read($, mode)) === 'exa' ? 'Exa / Firecrawl' : 'Native'}` }
52 const picked = parseMode(e.args)
53 if (!picked) return { text: 'Use /search native or /search exa' }
54 await setMode($, picked)
55 return { text: `Search mode set to ${picked === 'exa' ? 'Exa / Firecrawl' : 'Native'}` }
56 })
57
58 // Tell Claude which search tools to use.
59 on('prompt.compose', async ($, e, next) => {
60 const composed = await next(e)
61 const current = await read($, mode)
62 return { sections: [...composed.sections, { id: 'search-switch:mode', text: GUIDE[current], scope: 'session' as const }] }
63 })
64
65 // Block the other side's tools so the choice holds.
66 on('tool.call', async ($, e, next) => {
67 const current = await read($, mode)
68 if (current === 'exa' && NATIVE_TOOLS.has(e.tool)) {
69 return { deny: `Search mode is Exa / Firecrawl, so ${e.tool} is blocked. Use the Exa MCP tools or the firecrawl CLI.` }
70 }
71 if (current === 'native' && isExaTool(e.tool)) {
72 return { deny: 'Search mode is Native, so Exa tools are blocked. Use WebSearch or WebFetch.' }
73 }
74 if (current === 'native' && e.tool === 'Bash' && isFirecrawlCommand((e as { command?: unknown }).command)) {
75 return { deny: 'Search mode is Native, so the firecrawl CLI is blocked. Use WebSearch or WebFetch.' }
76 }
77 return next(e)
78 }).catch(($, e, next) => next(e))
79
80 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
81 const rest = await next(e)
82 if (e.props.hasSurvey) return rest
83
84 const { Box, Select, Text } = $.ui.resolve(e)
85 const current = await read($, mode)
86
87 return (
88 <Box>
89 <Select key="search-mode" label="Search" options={OPTIONS} value={current} onSelect={v => setMode($, v === 'exa' ? 'exa' : 'native')} />
90 <Text> </Text>
91 {rest}
92 </Box>
93 )
94 })
95}
96types/index.d.ts 8 lines1export type SearchMode = 'native' | 'exa'
2
3declare module 'claude-code' {
4 interface PluginState {
5 'search-switch': { mode: SearchMode }
6 }
7}
8