Skip to content

Web Search Tool

โญ New in 1.3 โ€” web_search is a tool the agent can call mid-conversation to pull up a search on the device. Itโ€™s a one-shot shortcut for โ€œthe user asked me to look something up.โ€

web_search opens a search results page in a browser on the connected phone. Thatโ€™s it.

It does not:

  • call a search API (no Serper, no Brave Search API, no server-side search)
  • return search results or snippets to the model
  • require an API key or any provider signup

It does: build a search-engine URL, then fire an Android VIEW intent at the best browser installed on the device so the results page appears on screen. The agent gets back a short status string (โ€œOpened Chrome โ†’ google search for: โ€ฆโ€), not the page contents.

Think of it as the fast path for โ€œsearch for Xโ€ โ€” replacing the four-step dance of launch Chrome โ†’ tap address bar โ†’ type โ†’ submit with a single tool call.

Agent calls web_search(device, query, engine)
โ”‚
โ–ผ
open_search() โ€” gitd/services/web_search.py:72
โ”‚
โ”œโ”€ 1. Build URL: _ENGINE_URLS[engine] + urlencode(query)
โ”‚
โ”œโ”€ 2. List installed browsers: adb shell pm list packages
โ”‚
โ”œโ”€ 3. Walk the browser priority chain, for each installed one:
โ”‚ am start -a android.intent.action.VIEW -d <url> -p <package>
โ”‚ (first one that handles the intent wins)
โ”‚
โ”œโ”€ 4. Fallback: bare VIEW intent โ†’ system default browser
โ”‚
โ””โ”€ 5. Last resort: open Play Store to install a browser

Because am start returns exit code 0 even when no activity handles the intent, _try_open() (web_search.py:57) inspects stdout for Error:, SecurityException, and no activities found to detect real failures.

The engine argument selects a URL prefix (_ENGINE_URLS, web_search.py:39). Unknown values fall back to Google.

engineOpens
google (default)google.com/search?q=
ddg / duckduckgoduckduckgo.com/?q=
bingbing.com/search?q=
bravesearch.brave.com/search?q=

open_search tries browsers in priority order (_BROWSER_CANDIDATES, web_search.py:27) and uses the first one thatโ€™s installed:

Chrome โ†’ Firefox โ†’ Samsung Internet โ†’ Edge โ†’ Brave โ†’ Opera โ†’ Opera GX โ†’ Vivaldi โ†’ DuckDuckGo Browser โ†’ system default โ†’ (Play Store, if nothing else)

So on a stock device youโ€™ll land in Chrome; on a stripped vendor build with no browser, Ghost opens the Play Store search for โ€œbrowserโ€ rather than failing silently.

web_search is a static entry in the agent tool list (gitd/services/agent_tools.py:249), in Anthropic tool-use format and auto-converted for other providers:

{
"name": "web_search",
"description": "Open a web search in the best available browser. Use when the user asks to search/look up somethingโ€ฆ",
"input_schema": {
"type": "object",
"properties": {
"device": { "type": "string" },
"query": { "type": "string" },
"engine": { "type": "string", "description": "google, ddg, bing, brave. Default google." }
},
"required": ["device", "query"]
}
}

The same tool is exposed to the MCP Server (gitd/mcp_server.py:403), so a claude-code / Cursor client driving Ghost gets it too. Both paths call the one open_search implementation.

Always available โ€” no flag, no config, no gate. web_search is a permanent member of the tool list every provider receives, so the model can call it any time. Itโ€™s steered purely by the description: โ€œUse when the user asks to search/look up something.โ€ Say โ€œlook up the weather in Berlinโ€ in Agent Chat and the model will typically fire web_search on the active device.

The return value is a status string, one of:

SituationReturned string
Success (named browser)Opened Chrome โ†’ google search for: <query>
Success (system default)Opened default browser โ†’ google search for: <query>
Empty queryweb_search error: empty query
No browser handled itweb_search failed: no browser handled the VIEW intent. Opened Play Store to install one. (Tried: โ€ฆ)

Use it when:

  • You want the agent to surface a search on the phone for a human to look at
  • Youโ€™re building a flow where the phone screen is the output surface (kiosk, demo, assisted browsing)

Donโ€™t reach for it when:

  • You want the agent to read and reason over search results โ€” it canโ€™t; results never come back to the model. For research-style reasoning, use a cloud provider whose model has its own web tool (see LLM Providers).
  • You need headless search with no visible browser โ€” this tool is fundamentally โ€œput a results page on the screenโ€.
  • Results are display-only. The model receives a status string, not page content. This is the single most important thing to understand about the tool.
  • No cost, no rate limit at Ghostโ€™s layer โ€” thereโ€™s no search API to bill or throttle.
  • Short ADB timeouts: pm list packages is 10 s, each am start is 8 s (web_search.py).
  • ddg and duckduckgo both work, though only ddg is advertised in the schema.
  • MCP Server โ€” exposes web_search (and every other tool) to external MCP clients
  • ADB Device Control โ€” the intent/am start layer this tool builds on
  • LLM Providers โ€” for models that reason over web results, pick a provider with its own web tool