Guides

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

AGENTS.md
ledger.py
.env
permissions.yaml
  1. restricted/ledger.py
  2. Permission ledger
  3. Grant in Settings
  4. Checked at every call
  5. Row in the audit log
The file name becomes the permission. A grant opens it. Every call is checked and recorded.

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.

finance/restricted/ledger.py
"""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.text

One 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:

finance/restricted/.env
LEDGER_API_URL=https://ledger.internal.example.com
LEDGER_API_TOKEN=paste-the-token-here

The 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:

finance/identity/permissions.yaml
ledger:
  label: Read ledger
  description: View accounting entries and balances.
payroll:
  label: Run payroll
  description: Process employee payroll and view salaries.
  sensitive: true
Every 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:

Terminal
hubzoid doctor ./finance

The 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

  1. Open Settings and choose Agents, then the agent, then the Access tab.
  2. 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.
  3. Open Restricted tools and tick Read ledger. Use this agent is already ticked and stays ticked, because every capability includes entry to the agent.
  4. Select Review changes. The review lists the person, the agent and the capabilities being added and removed, and marks sensitive ones.
  5. 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.

Settings, the My Hub agent on its Access tab, People view: Lee Okafor, Priya Raman and Sam Rivera with their capabilities, an Edit access button on each row and Add user above the list.Settings, the My Hub agent on its Access tab, People view: Lee Okafor, Priya Raman and Sam Rivera with their capabilities, an Edit access button on each row and Add user above the list.
Edit access on a row opens the capability editor. Add user creates an account with its first access.

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.

Terminal
hubzoid grant priya@example.com ledger ./finance
Output
granted priya@example.com · ledger in finance

use_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:

Terminal
# 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 ./finance

hubzoid 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:

finance/workflows/monthly_close/main.py
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.

  1. 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 decision allow and the reason grant is written.

  2. 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-grant row is written.

  3. 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.

Settings, Activity, Audit log tab: the Access changes and Tool decisions switch, filters for agent, time and person, and rows such as Server operator allowed Priya Raman to Use this agent in My Hub.Settings, Activity, Audit log tab: the Access changes and Tool decisions switch, filters for agent, time and person, and rows such as Server operator allowed Priya Raman to Use this agent in My Hub.
Access changes records who changed access. Tool decisions records every call to a restricted tool.

The same rows are available on the server with hubzoid audit.

Read tool decisions from the server
Terminal
hubzoid audit ./finance --user priya@example.com
hubzoid audit ./finance --denied --limit 100

Each 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 seeWhat to do
Denied with no-grantThe 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:telegramThat channel cannot reach restricted tools. Add it to HUBZOID_RESTRICTED_SURFACES only if every request there carries one verified person, then restart.
Denied with anonymousThe 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 SettingsThe file starts with _, is outside restricted/, or failed to import. Run hubzoid doctor.
A workflow's call is denied with no-grantThe 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 seeWhat to do
Denied with surface:workflowHUBZOID_RESTRICTED_SURFACES was set without workflow. Add it back and restart.
Denied with blockedThe account is blocked, or its chat account is unavailable. Check the person under People.
Denied with store-errorThe operational database could not be read. Check the database and the bridge logs.
hubzoid grant '*' ... is refusedNew 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 as restricted/.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

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.