Restrict a tool
Put a sensitive tool behind a permission, grant it to named people in Settings or with hubzoid grant, and check every call in the audit log.
This guide puts a ledger lookup in a finance agent behind a permission, so only the people you choose can call it. By the end, a person with the grant can use the tool, everyone else cannot, and every call shows in Settings with who made it and why it was allowed or refused.
Video lesson 9 · 3:04Access and telemetry: run AI agents for a whole teamGrant one restricted tool and trace its use in Activity.
When to use this
Restrict a tool when it reads or changes something that not every user of the agent should reach, such as payroll data, refunds or a production system. Tools that anyone who can open the agent may use belong in tools_local/ instead. The rules behind this are in identity and access.
What you need
- Sign-in turned on (
HUBZOID_AUTH=true), so every person arrives with a verified email. In local mode every request on this machine is the local owner, so you cannot test as different people. See authentication. - An organization administrator account, or Manage access on the agent. The first administrator comes from
hubzoid admin create you@example.com ./finance --owner. An agent administrator grants only capabilities they hold there themselves, so they also need the new permission.
What you will build
- restricted/ledger.py
- Permission ledger
- Grant in Settings
- Checked at every call
- Row in the audit log
Gate the tool
Write the tool in restricted/
A restricted tool is an ordinary @function_tool, the same as a tool in tools_local/. The difference is the folder. The file name, in lowercase and without .py, is the permission, so every tool in restricted/ledger.py requires ledger.
"""Ledger lookups. Every tool in this file requires the `ledger` permission."""
from __future__ import annotations
import os
import httpx
from agents import function_tool
@function_tool
def ledger_read(account: str, period: str) -> str:
"""Return ledger entries for one account and accounting period.
Args:
account: Ledger account code, for example "4000".
period: Accounting period as YYYY-MM, for example "2026-08".
Returns:
The entries as JSON text.
"""
# Read credentials inside the call. They are loaded from restricted/.env.
base_url = os.environ["LEDGER_API_URL"]
token = os.environ["LEDGER_API_TOKEN"]
response = httpx.get(
f"{base_url}/entries",
params={"account": account, "period": period},
headers={"Authorization": f"Bearer {token}"},
timeout=20,
)
response.raise_for_status()
return response.textOne permission per file. Tools that need a different permission go in a different file, for example restricted/payroll.py for payroll. More rules are under how files map to permissions.
Keep credentials in restricted/.env
Put the secrets the tool needs next to it:
LEDGER_API_URL=https://ledger.internal.example.com
LEDGER_API_TOKEN=paste-the-token-hereThe model only receives what the tool returns, and the agent's file tools cannot open this folder. See where secrets live.
The order Hubzoid loads the two .env files
At start, Hubzoid loads the Hub's .env, then restricted/.env, into the bridge's environment. A value in restricted/.env wins when both set it.
Name the capability
Without a label, Settings shows the permission name in title case, such as "Ledger". Add identity/permissions.yaml to give each capability a label, a description and, where it matters, a sensitive flag:
ledger:
label: Read ledger
description: View accounting entries and balances.
payroll:
label: Run payroll
description: Process employee payroll and view salaries.
sensitive: trueEvery field of a capability label, and entries that are ignored
Prop
Type
Sensitivity is only what you declare here. Hubzoid never guesses it from a name. Entries for built-in capabilities such as use_hub, manage_access or curator are ignored with a warning, and an entry with no matching restricted/ file has no effect.
Restart and check
The bridge loads restricted tools when it starts, so restart the Hub after adding or renaming a file. Then check that the tools load:
hubzoid doctor ./financeThe access section reports the restricted tools and the permissions they use, such as 1 restricted tool(s) with ledger.
A file that does not import, and when Settings shows the capability
A file that does not import shows as a failure with the error. Settings reads the capability list from the files on every request, so the new capability appears there right away.
Grant access in Settings
- Open Settings and choose Agents, then the agent, then the Access tab.
- Choose who gets the tool:
- Someone already listed: select Edit access on their row.
- Someone with an account but no access to this agent: open them under People and select Add an agent.
- Someone new: select Add user and enter their name and email.
- Open Restricted tools and tick Read ledger. Use this agent is already ticked and stays ticked, because every capability includes entry to the agent.
- Select Review changes. The review lists the person, the agent and the capabilities being added and removed, and marks sensitive ones.
- Select Save change, or Create account for a new user. Settings confirms with "Access updated", or "User added" for a new user.
How a new user signs in for the first time
A new user sets their own password with a one-time sign-in link. Where Google sign-in is set up, Google sign-in only creates the account with no password. Nothing is emailed. The one-time sign-in link is shown once, to copy and share yourself.


The new permission applies on the person's next call. Every screen in this flow is described in agents and access.
What else the Access editor holds
Besides Restricted tools, the editor groups the agent's other capabilities. Each group is described in agents and access.
The other capability groups and the Not configured status
The groups are Hub access (Use this agent), Connectors, Administration (Manage access) and Hubzoid tools, the built-in tools that stay off until granted, such as Save shared knowledge and Call Jev. Empty groups are hidden.
A capability can show a status such as Not configured when a setting it needs is missing. You can still grant it, but it cannot run until an operator adds the setting.
Grant access from the CLI
The same access store is available on the server. Commands take the Hub folder as their last argument and use its name as the agent, or another agent with --hub.
hubzoid grant priya@example.com ledger ./financegranted priya@example.com · ledger in financeuse_hub is added automatically. The CLI writes the permission name exactly as you type it, while Settings offers only capabilities that exist, so check the spelling against the file name.
Check, list and revoke access, and grants for new emails
The other commands:
# Every permission a person effectively holds in the agent
hubzoid access check priya@example.com ./finance
# Every grant in one agent
hubzoid access list ./finance --hub finance
# Remove one permission, or use_hub to remove everything in the agent
hubzoid revoke priya@example.com ledger ./finance
hubzoid revoke priya@example.com use_hub ./financehubzoid access check prints a line such as priya@example.com in finance: ledger, use_hub. CLI changes are recorded with the operating system user as the actor and appear in the audit log as a server operator. All options are in the CLI reference.
Unlike Settings, hubzoid grant also accepts an email that has no account yet. The access applies when someone first signs in with that email, for example through single sign-on.
New access for everyone signed in (the * subject) is refused everywhere, so grant named people. An agent that still has an Everyone signed in row from an older release keeps it until an organization administrator removes it, in Settings or with hubzoid revoke '*' use_hub ./finance.
Grant a workflow
Workflows and markdown tasks run as an ordinary account: their run_as, else HUBZOID_WORKFLOW_USER, else the owner recorded at setup. That account's grants decide what they may call, so grant it ledger like any person. The full rules are in workflows and schedules.
Which account to use, checking the grant in code and old grants
It must have signed in once and hold Use this agent. For shared automation, create an ordinary account such as reports@example.com. Runs as on the agent's Workflows tab and hubzoid schedule list ./finance show who each workflow runs as. A workflow that people run for themselves from chat acts as each person instead, so each of them needs the permission.
When the workflow calls the agent with hub.call_agent, the same guard decides and logs the call on the workflow channel. A workflow can also check the grant before doing work:
from hubzoid import hub, workflow
@workflow(schedule="0 6 1 * *", timezone="Asia/Kolkata", run_as="reports@example.com")
def monthly_close():
if not hub.user.can("ledger"):
raise RuntimeError(f"{hub.user.email} needs the ledger permission")
return hub.call_agent("Summarize last month's ledger entries for account 4000.")Grants that older releases made to workflow:<name> service identities are kept but no run uses them. Settings labels them Legacy service identity. Grant the account instead, then revoke the old grant.
Test allowed and denied calls
Test with real people. hubzoid test runs with no signed-in person, so it cannot reach a restricted tool. An eval with run_as checks the same rule from the command line.
-
Sign in as the person you granted and ask:
Message Show the ledger entries for account 4000 in 2026-08.The agent calls
ledger_read, and a row with the decisionallowand the reasongrantis written. -
Sign in as someone who holds only Use this agent. The tool is hidden from them on every runtime, so the agent answers without it and no row is written. A call that reaches the tool another way is refused with an access denied message, and a
no-grantrow is written. -
Sign in as someone with no grant in the agent. Their chat requests to it are refused.
To test the workflow, start it by hand with hubzoid schedule run ./finance monthly_close. Its tool calls are recorded with the account it runs as and the channel workflow.
Read the decisions in the audit log
Open Settings → Activity → Audit log and switch to Tool decisions, or select View activity on the agent's page. Each row says who used which tool, with an Allowed or Denied tag.
Filters and row details
Filter by agent, time, person, outcome, tool or channel, and open a row to see the exact time, tool, channel and reason.


The same rows are available on the server with hubzoid audit.
Read tool decisions from the server
hubzoid audit ./finance --user priya@example.com
hubzoid audit ./finance --denied --limit 100Each line shows the time, the caller, ALLOW or DENY, the tool and the channel with the reason. Add --json for JSON only.
If something goes wrong
| What you see | What to do |
|---|---|
Denied with no-grant | The person lacks the permission, or a CLI grant used another spelling. Compare hubzoid access check with the file name in restricted/. |
Denied with surface:slack-dm, surface:whatsapp or surface:telegram | That channel cannot reach restricted tools. Add it to HUBZOID_RESTRICTED_SURFACES only if every request there carries one verified person, then restart. |
Denied with anonymous | The request had no signed-in person, such as hubzoid test or an eval without run_as. Test as a signed-in person, or give the eval run_as. |
| The capability is missing from Settings | The file starts with _, is outside restricted/, or failed to import. Run hubzoid doctor. |
A workflow's call is denied with no-grant | The account it runs as lacks the permission, or only an old workflow:* subject holds it. Check Runs as and grant that account. |
More problems and fixes
| What you see | What to do |
|---|---|
Denied with surface:workflow | HUBZOID_RESTRICTED_SURFACES was set without workflow. Add it back and restart. |
Denied with blocked | The account is blocked, or its chat account is unavailable. Check the person under People. |
Denied with store-error | The operational database could not be read. Check the database and the bridge logs. |
hubzoid grant '*' ... is refused | New access for everyone signed in cannot be created. Grant named people. |
How files map to permissions
One permission per file. The file name in lowercase, without .py, is the permission.
Skipped files, duplicate names and Hubs without restricted/
- Files whose names start with
_are skipped. Files that are not Python, such asrestricted/.env, are ignored. - A restricted tool replaces a built-in or
tools_local/tool with the same name. If two restricted files define the same tool name, the first file in alphabetical order wins and the duplicate is logged. - A Hub without a
restricted/folder has no restricted tools, and nothing about it changes.
Where secrets live
Secrets for restricted tools live in restricted/.env, which the agent's own file tools cannot open.
AWS Secrets Manager and how file tools are kept out
To keep the values in AWS Secrets Manager instead of the file, name a secret with HUBZOID_RESTRICTED_SECRET_NAME in restricted/.env. Its keys win over the file.
The agent's own file tools stay out of the folder. read_file refuses any path inside restricted/, whatever its capitalization. list_files leaves those files out, and grep_data refuses a search path inside the folder. On claude-local and codex-local models, the local CLI's own file and shell tools are switched off, so the agent works only through Hubzoid's tools. A .env line in .gitignore also matches restricted/.env, which keeps the file out of Git.
Secrets live in the process environment
Any tool code running in the bridge can read the environment. Agent child processes, such as the claude CLI and its MCP servers, do not inherit the values from restricted/.env, but Hub tools do. Never return a credential in a tool result, and review Hub tools that read os.environ. For a credential that must never share a process with the agent, run the integration as a separate service under a different operating system user and let the restricted tool call it.
Where restricted tools can be reached
A restricted tool runs only on channels where each request carries one verified person. The default list covers the web app, the API, MCP, workflows and apps that embed the chat. Slack, WhatsApp and Telegram are not in it. If you set HUBZOID_RESTRICTED_SURFACES, keep the channels you rely on in the list.
People who chat from an embedding app
People who chat from an embedding app have no Hubzoid account and no grants. The app's own limit in hubzoid.yaml decides which tools they may use, so name tools there rather than * when the agent has restricted tools.
Next steps
Identity and access
Accounts, grants, channels and the full decision path behind this guide.
Agents and access
Every control on the Access tab, from Add user to the review step.
People and activity
Account states, deleting users and using the audit log for reviews.
Connect an assistant
Use the Hub from Claude Code with the same access rules.

