This is the canonical list of every tool available to the bot, organised by what's on by default and what's opt-in. Each opt-in section names the env var(s) that flip the gate.
For Claude Code's full upstream tool catalogue (some of which hamroh doesn't currently expose) see https://code.claude.com/docs/en/tools-reference. The "Other CC tools you can wire in" section at the bottom of this page points at the ones a fork might want to add.
These are the bot's core surface, served by the local hamroh MCP
server. Auto-discovered from hamroh/tools/*.py (each tool is a
BaseTool subclass). All available every turn; no env flag needed.
| Tool | What it does |
|---|---|
telegram_send_message |
Send a text message to a chat. The only way the user sees anything — a plain text content block produces no Telegram output. |
telegram_reply_to_message |
Reply to a specific user message (threads in groups). |
telegram_edit_message |
Edit one of the bot's previous messages. No push notification — good for in-progress updates on long tasks. |
telegram_delete_message |
Delete a bot message. Use sparingly. |
telegram_add_reaction |
React to a message with an emoji. Prefer over "ok"/"👍" replies in groups. |
telegram_create_poll |
Send a poll. Supports regular/quiz, multi-answer, anonymity toggle, auto-close (open_period or close_date), and reply-to. |
telegram_stop_poll |
Close a live poll early and return final tallies. |
telegram_read_attachment |
Read a photo or document the user sent. The dispatcher saves inbound attachments under data/attachments/ and surfaces them as [attachment: <path> ...] markers — pass that path here. Images come back as image content blocks (you actually see them); text-like files (md/txt/log/csv/json/yaml/code) come back as UTF-8; PDFs are extracted via pypdf and returned as text with --- page N --- markers. Path traversal is rejected. GIFs/videos are unsupported. |
telegram_send_memory_document |
Send a memory file (under memories/) to a chat as a downloadable document. Path-locked to the memory root. Optional caption + reply-to. |
render_html |
Render an HTML snippet to PNG via headless Chromium → data/renders/. Use for tables/charts/diffs that markdown can't fit. Network blocked — inline any CSS/JS. Returns the relative path. |
render_latex |
Render a LaTeX expression to PNG via KaTeX (loaded from cdn.jsdelivr.net only — narrow allow-list). Pass the LaTeX without surrounding $$. Optional title. Returns the relative path; pair with telegram_send_photo. |
telegram_send_photo |
Send a rendered photo (from data/renders/) as an inline Telegram photo with preview. Pair with render_html or render_latex. |
Drive a real headless Chromium for pages WebFetch can't reach
(JS-rendered, multi-step, form-driven). One warm browser is reused for
the whole session; browser_navigate opens a page and the rest of the
tools act on that same page across the turn — so flows like search →
open the images tab → grab the first image → send it work. Live network
is allowed here (unlike renders), but localhost / RFC1918 / link-local /
file:// targets are refused. On by default; disable by listing the
tools in builtin_tools_disabled.
| Tool | What it does |
|---|---|
browser_navigate |
Open a URL in the shared Chromium page and wait for the DOM. Start here, then use the other browser_* tools on the same page. ~30s load budget. |
browser_back |
Go back one entry in the page history. Returns the URL landed on. |
browser_reload |
Reload the current page (content changed or didn't finish loading). |
browser_reset |
Close the page and clear cookies/state; the next browser_navigate opens a fresh isolated tab. |
browser_click |
Click an element. Waits up to ~10s for it to be actionable. |
browser_fill |
Type a value into an input/textarea (replaces existing content). Pair with browser_click to submit. |
browser_press_key |
Press a key (Enter to submit, Tab to move focus); optional selector targets a field. |
browser_select_option |
Choose an option in a native <select> dropdown (use instead of browser_fill). |
browser_scroll |
Scroll the page (or a selector into view) to reveal lazy-loaded content. |
browser_get_text |
Return the visible text of the page, or one element via CSS selector. Truncated. |
browser_get_html |
Return the page HTML, or one element's inner HTML. Use when structure/attributes matter. Truncated. |
browser_get_attribute |
Read one HTML attribute of an element — e.g. an image's src, a link's href. |
browser_list |
List elements matching a selector with their text and href/src — pick which link/image to act on. |
browser_wait_for |
Wait for an element to appear (after an async-loading click) before reading or acting. |
browser_screenshot |
Screenshot the page (or one element) to a PNG under data/renders/; pair with telegram_send_photo. |
browser_download |
Download the original file at a URL (typically an image) into data/renders/ and return its path; pair with telegram_send_photo. Get the URL first with browser_get_attribute. |
| Tool | What it does |
|---|---|
memory_list |
List existing memory files, each with its frontmatter description (progressive disclosure, like skill_list). Legacy files without frontmatter show just path + size. |
memory_search |
Search the text inside memory files for keywords; returns matching lines, best matches first. |
memory_read |
Read a memory file by relative path. |
memory_write |
Create or overwrite a memory file. Content must begin with name/description frontmatter (the template); writes without it are rejected. Read-before-write rail enforced; 64 KiB cap. |
memory_append |
Append text to a memory file's body and refresh its frontmatter description (so memory_list stays current). Name is preserved or derived from the filename; the first append migrates a legacy file onto the template. |
telegram_send_memory_document |
Deliver a memory file to a chat as a downloadable Telegram document. Path-locked to the memory root. Optional caption + reply-to. |
One store. All memory lives in the single memories/ folder at the repo
root. It is git-tracked, so memories survive a volume loss and the operator can
commit them; in Docker it's bind-mounted so runtime writes land in the host
checkout. The bot reads, searches, writes, and appends here — there is no
read-only tier. See memories/README.md.
Every memory is named by its full path starting with memories/. The prefix is
required; a bare path like notes/ref.md is rejected, so pass paths verbatim
from memory_list / memory_search.
Memory files follow the same frontmatter protocol as skills — a --- block
with name and description — so the agent can scan memory_list and pick
the right file without reading every one:
---
name: <short human-friendly label>
description: <one-line summary used to find this memory without reading it>
---
<body — the actual remembered content>
There is no delete_memory by design — overwriting is the supported
"forget" path. Operator handles real deletion on host.
| Tool | What it does |
|---|---|
instruction_read |
Read the current contents of prompts/project.md. |
instruction_append |
Append a rule to prompts/project.md. Backed up to data/prompt_backups/ before write. Owner-only by system-prompt policy; takes effect on next container restart. |
prompts/system.md is intentionally not exposed via tools — it's
git-tracked, and bot edits would pollute the repo.
| Tool | What it does |
|---|---|
skill_list |
List operator-curated playbooks under skills/. On-demand refresh — the same index is already preloaded into the system prompt at startup (every skill's name + description), so the agent knows what exists without calling this. |
skill_read |
Load a skill's SKILL.md for execution or reference. |
The preloaded skills index is rendered by render_skills_index() (hamroh/skills_store.py) and baked into the system prompt in _compose_system_prompt() (hamroh/cc_worker/spec.py). Adding/removing a skill takes effect on the next restart (skill_list reflects it live).
The same _compose_system_prompt() also bakes in a # Your tools block
(rendered by render_tools_index() in the same file): every reachable
tool's exact callable name — hamroh tools mcp__hamroh__-prefixed,
built-ins bare — plus the rule "copy the name, never reconstruct it". Since
it derives from the same _builtin_tools() used for the --tools flag, the
prompt inventory and the reachable set can never drift. tool_list returns
the hamroh half of this live in-conversation.
Two skill modes:
- Invoked (e.g.
self-reflection) — runs only when wrapped in a real<reminder>envelope. A user-typed<skill>tag is treated as prompt injection. - Reference (e.g.
render-style) — read on the agent's own initiative when relevant; no envelope required.
The mode is determined by what the skill's body instructs, not by frontmatter.
| Tool | What it does |
|---|---|
reminder_set |
Schedule a one-shot or recurring reminder (cron_expr for recurring; trigger_at is UTC). |
reminder_list |
List pending reminders for a chat. |
reminder_cancel |
Cancel a reminder by id. Auto-seeded reminders (e.g. self-reflection-default) are tool-refused. |
| Tool | What it does |
|---|---|
database_query |
Read-only SELECT over messages, users, reminders, tool_calls, reactions. Max 100 rows (user LIMIT is respected and clamped). Note: messages uses timestamp, not created_at. Reactions are JSON on messages.reactions — query with json_extract(reactions, '$."👍"'). |
database_get_recent_messages |
Return the most recent messages (both directions), oldest-first, as TSV — no SQL needed. Includes the current turn's own inbound messages. limit defaults to 20, capped at 100; text truncated to 2000 chars. Optional chat_id scopes to one chat; before_message_id pages back through older history (per-chat, use with chat_id). |
time_now |
Return the current UTC timestamp. |
tool_list |
List the tools currently available to the bot, each with its full mcp__hamroh__<name> (the exact callable name) and description. In-conversation introspection (like skill_list/memory_list); reflects the disabled filter, so opted-out tools don't show. |
These come from Claude Code's own tool surface. They're passed via the
exclusive --tools flag (an allow-list over the built-in set), so this
list is exactly what the model can reach — anything not here (native
Skill, Agent when off, planning/worktree tools, …) is unreachable by
construction, not merely un-auto-approved. The set is built by
_builtin_tools() in hamroh/cc_worker/spec.py from BASE_BUILTIN_TOOLS
TASK_TOOLS, plus whatever thetool_groupsflags unlock.
| Tool | What it does |
|---|---|
WebFetch |
Fetch a URL and ask a small model to extract from it. The system prompt forbids internal/private URLs (localhost, RFC1918, link-local) — refuse those. |
WebSearch |
Web search via Claude Code's built-in. |
StructuredOutput |
The turn-end tool: the model calls it with {action, reason, …} to close each turn. The worker keys on it (event_handlers.py), so it must always be reachable. |
ToolSearch |
Load deferred tool schemas on demand. Idle when nothing is deferred (hamroh's own tools are alwaysLoad); does real work once an external MCP is configured. |
ListMcpResourcesTool / ReadMcpResourceTool |
Reach MCP resources (URI-addressable data) an external MCP server may expose. hamroh's own server exposes none, so these are no-ops until a resource-bearing MCP is enabled. |
WaitForMcpServers |
Block on an external MCP server that is still connecting. |
TaskCreate / TaskGet / TaskList / TaskUpdate |
Session task-checklist, so the bot can track a multi-step turn (e.g. a research digest fanning out over many sources). No permission required. |
The MCP-discovery and task tools are read-only / no-permission and harmless
when idle. TaskStop/TaskOutput are deliberately omitted (background-task
control the fire-and-forget bot doesn't need; TaskOutput is deprecated).
Two configuration layers feed the bot's tool surface:
plugins.jsonat repo root — single source of truth for tool-group toggles, the list of external MCP servers, thebuiltin_tools_disabledlist, andskills_disabled. The operator copies the shippedplugins.json.exampleonce (cp plugins.json.example plugins.json), edits, and restarts.plugins.jsonis gitignored so customisations stay local..env— credentials for external services (Jira, GitLab, GitHub) only. Referenced fromplugins.jsonvia${VAR}.
Changes to either take effect on container restart.
tool_groups— flips for the Claude-Code built-ins below (bash,code,subagents). Edit and restart to flip.mcps[].nameis load-bearing — it becomes themcp__<name>__<tool>namespace the model sees. Renaming breaks operator memory, prompts, and tests. The defaults match today's keys exactly (mcp-atlassian,mcp-gitlab,github).mcps[].typeselects the transport:stdio(default),http, orsse. Mirrors what Claude Code's--mcp-configaccepts. See "MCP transports" below for the per-transport field shape.mcps[].allowed_toolsis one flat list — exact tool name (mcp__mcp-atlassian__jira_search) or a server-prefix shorthand (mcp__mcp-gitlab). Both forms are accepted by Claude Code's--allowedTools.${VAR}interpolation runs overargs(each element),envvalues,url, andheadersvalues — pulling from the process env (i.e..env). Concatenation works (${GITLAB_URL}/api/v4). If any referenced${VAR}resolves empty, that MCP is silently skipped at boot — preserving today's "credentials missing → MCP not spawned" semantics.enabled: falseskips the MCP even if its${VAR}refs resolve.- A missing
plugins.jsonboots with empty plugins (locked-down). A malformedplugins.jsoncrashes boot loudly with aPluginsConfigError.
Three transports are supported, exactly as the MCP
spec and Claude Code's
--mcp-config define them. Mixing fields across transports
(e.g. command on an http entry) crashes boot.
stdio — local subprocess (default). hamroh spawns the
command, talks over stdin/stdout. Auth via the subprocess env
block.
{
"name": "github",
"type": "stdio", // optional; default when omitted
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" },
"allowed_tools": ["mcp__github"],
"enabled": true
}http — remote streamable-HTTP server. Auth via static
headers. Use this for hosted MCPs (Linear, Notion-cloud, GitHub's
remote MCP, etc.) where you've already issued a PAT or OAuth token.
{
"name": "linear",
"type": "http",
"url": "https://mcp.linear.app/mcp",
"headers": { "Authorization": "Bearer ${LINEAR_API_KEY}" },
"allowed_tools": ["mcp__linear"],
"enabled": true
}sse — Server-Sent Events transport. Same field shape as
http. Some hosted MCPs use this; check vendor docs.
{
"name": "events",
"type": "sse",
"url": "https://example.com/sse",
"headers": { "X-API-Key": "${EVENTS_KEY}" },
"allowed_tools": ["mcp__events"],
"enabled": true
}Auth. Hamroh doesn't manage OAuth flows — supply an
already-issued token via ${VAR} interpolation. For interactive
OAuth-managed servers, see Claude Code's MCP docs (it can run the
flow on your behalf when configured outside plugins.json).
These three groups default to off. Flip in plugins.json
(tool_groups.<name>: true).
| Tool | What it does |
|---|---|
Bash |
Run shell commands. |
PowerShell |
Run PowerShell commands (Windows / opt-in via CLAUDE_CODE_USE_POWERSHELL_TOOL). |
Monitor |
Watch a long-running process and stream output back to the model. |
These all share Claude Code's "permission required" risk class — same trust class. Off by default for safety.
| Tool | What it does |
|---|---|
Edit |
Targeted edits to a file. |
Write |
Create or overwrite a file. |
Read |
Read a file. |
NotebookEdit |
Edit Jupyter notebook cells. |
Glob |
Find files by glob pattern. |
Grep |
Search file contents. |
LSP |
Code intelligence — definitions, references, type errors. Requires a code-intelligence plugin to be installed. |
Useful unit when you want the bot to do real code work (not just chat). The Telegram-assistant deployment leaves this off and relies on memory
- project.md for everything it needs to remember.
| Tool | What it does |
|---|---|
Agent |
Spawn a subagent with its own context window for an isolated task. Token-heavy — leave off unless you need it. When off, the subagent docs (prompts/subagents.md) aren't even loaded, so the model doesn't know the capability exists. |
SendMessage |
Resume/steer a background subagent (or message an agent-team teammate). Unlocked together with Agent. |
- Append an entry to
mcpsinplugins.jsonwith a uniquename, thecommandto spawn (and anyargs), theenvit needs (use${VAR}to pull credentials from.envrather than committing them), andallowed_toolslisting the exact tool names ormcp__<name>prefix to advertise. - Add any referenced env vars to
.env. - Restart:
docker compose up -d --force-recreate. - Tail logs — you should see
mcp <name> configured (...). If it saysskipped (unresolved ${VAR} ...), an env var is empty.
The built-ins under "Always on — hamroh built-ins" are
auto-discovered from hamroh/tools/*.py and registered every
boot. To hide one (e.g. you don't use polls, or you don't want LaTeX
rendering eating context), list its name in builtin_tools_disabled:
{
"builtin_tools_disabled": [
"telegram_create_poll", "telegram_stop_poll",
"render_latex", "render_html", "telegram_send_photo"
]
}The tool is skipped at MCP registration time — it's never
instantiated, never advertised, and the model has no way to invoke
it. Names must match an exact tool name (the name class attribute
on the BaseTool subclass — also the cell text in this doc's
tables). A typo crashes boot with the available list.
There is no curated "essential" set — disabling telegram_send_message mutes
the bot, disabling telegram_read_attachment makes it blind to inbound
photos and documents. The operator owns this trade-off.
Add the skill's directory name to skills_disabled:
{ "skills_disabled": ["render-style"] }Restart. skill_list no longer surfaces it and skill_read raises
"not found", so envelope-driven invocations
(<skill name="...">) can't bypass the toggle either.
The default plugins.json ships an mcp-atlassian entry pointed at
Atlassian's official remote MCP (https://mcp.atlassian.com/v1/sse,
type: sse). It's enabled: false by default — flip it to true to
advertise the mcp__mcp-atlassian tools on --allowedTools.
Auth is OAuth, not env vars. The remote server authenticates via
OAuth, which hamroh does not manage. Establish the grant once on the
host with Claude Code (claude mcp add --transport sse atlassian https://mcp.atlassian.com/v1/sse, then complete the browser login);
Claude Code reuses the stored token. The headless bot can't run the
browser flow itself, so the OAuth grant must already exist on the host.
For the tool list and capabilities see Atlassian's remote MCP docs.
The default plugins.json ships an mcp-gitlab entry that
references both vars. When they're set in .env, the mcp-gitlab
server spawns and the mcp__mcp-gitlab prefix is added to
--allowedTools. Unlike the Jira server, mcp-gitlab is GitLab-only —
the prefix match is safe.
For the canonical GitLab tool list see upstream https://github.com/zereight/mcp-gitlab.
The default plugins.json ships a github entry that references the
token. When set in .env, the github MCP server spawns (via npx -y @modelcontextprotocol/server-github) and the mcp__github
prefix is added to --allowedTools. Single-vendor server, blanket
prefix match is safe.
GitHub.com is the default. For GitHub Enterprise, add a
"GITHUB_HOST": "${GITHUB_HOST}" line to the github plugin's env
block in plugins.json and set GITHUB_HOST (e.g.
github.example.com) in .env. (The default omits this line so
github.com users aren't blocked by an unset var.)
How to generate the token (fine-grained PAT — recommended):
- Go to https://github.com/settings/tokens?type=beta.
- Token name:
hamroh(or your bot's name). - Expiration: 90 days (max for fine-grained). Set a calendar reminder to rotate.
- Resource owner: your account, or the org if the bot acts on org repos.
- Repository access: "Only select repositories" → pick exactly the repos the bot should touch. Never "All repositories" unless that's truly what you want.
- Repository permissions — grant only what the bot needs:
Contents— Read & write (read code, push branches, commit)Issues— Read & write (file bug tickets from chat)Pull requests— Read & write (open PRs, comment)Metadata— Read (mandatory; auto-granted)Actions— Read & write (only if the bot should trigger or read CI)- Everything else: "No access"
- Click Generate token. Copy the
github_pat_...string — you won't see it again. - Paste into
.envasGITHUB_PERSONAL_ACCESS_TOKEN=github_pat_...and restart:docker compose up -d --force-recreate.
Avoid classic PATs. They grant scopes per-org with no per-repo
limit, so a leaked classic token has a much bigger blast radius. The
?type=beta URL above gets you the fine-grained kind.
GitHub Enterprise: generate the PAT on your Enterprise instance
and also set GITHUB_HOST=github.your-company.com in .env.
Swapping the MCP server. If you'd rather use the official Go-based
github/github-mcp-server
instead of the npm package, edit the github entry in plugins.json:
swap command/args and adjust the env block. The rest of the
plumbing (allowlist, credential interpolation) stays as-is.
Tool groups — flip in plugins.json:
{ "tool_groups": { "bash": true, "code": true, "subagents": false } }External MCPs — credentials in .env, advertise/disable in
plugins.json:
# Jira (set all three; mcp-atlassian spawns when present)
JIRA_URL=https://your-site.atlassian.net
JIRA_USERNAME=you@example.com
JIRA_API_TOKEN=...
# GitLab (set both)
GITLAB_URL=https://gitlab.example.com
GITLAB_TOKEN=...
# GitHub
GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_...To stop advertising an MCP without removing credentials, flip
enabled: false on its entry in plugins.json. To remove entirely,
delete the entry.
Skills — list directory names in plugins.json skills_disabled.
Restart the container after any edit: docker compose up -d --force-recreate. Tail logs at startup — plugins loaded: N enabled mcp(s), M disabled skill(s), tool_groups={...} summarises the active
config; mcp <name> configured (...) / mcp <name> skipped (...)
lines explain each MCP's outcome.
hamroh doesn't expose every Claude Code built-in. The remaining
omissions are deliberate (see the full-catalog audit in the
BASE_BUILTIN_TOOLS comment in hamroh/cc_worker/spec.py). A fork that
wants any of these can add it to BASE_BUILTIN_TOOLS (always on) or a
gated set (BASH_TOOLS, CODE_TOOLS, SUBAGENT_TOOLS) in that same file;
_builtin_tools() assembles the final --tools list.
Already exposed (don't re-add): the task-checklist tools (TaskCreate,
TaskGet, TaskList, TaskUpdate) and MCP-discovery tools
(ToolSearch, ListMcpResourcesTool, ReadMcpResourceTool,
WaitForMcpServers) are on by default; SendMessage unlocks with
subagents.
Notable upstream tools still off on purpose:
Skill— hamroh runs skills through its own<reminder><skill>envelope +mcp__hamroh__skill_read, not CC's nativeSkilltool; leaving it on is a dead-end (empty registry).- Scheduled tasks —
CronCreate,CronDelete,CronList. Would duplicate hamroh's ownreminder_*tools. PushNotification/SendUserFile— hamroh delivers over Telegram; these need Anthropic push infra / Remote Control.AskUserQuestion— interactive multi-choice UI, dead in--printheadless mode; the bot asks via Telegram instead.- Planning & worktrees —
EnterPlanMode,ExitPlanMode,EnterWorktree,ExitWorktree. Dev-loop tools; useful for code-work forks. Artifact,RemoteTrigger,ScheduleWakeup,ShareOnboardingGuide,Workflow,TodoWrite,ReportFindings— claude.ai / Team-plan / deprecated / not applicable to a chat bot.TaskStop/TaskOutput— background-task control the fire-and-forget bot doesn't need (TaskOutputis deprecated).
For each, the upstream https://code.claude.com/docs/en/tools-reference is authoritative — it lists permission requirements and behaviour notes that may change between CC versions.
{ "tool_groups": { "bash": false, "code": false, "subagents": false }, "mcps": [ { "name": "mcp-atlassian", "type": "sse", "url": "https://mcp.atlassian.com/v1/sse", "allowed_tools": ["mcp__mcp-atlassian"], "enabled": true } ], "skills_disabled": [] }