---
title: Hub files
description: Reference for every file and folder a Hub can contain, the frontmatter keys each loader reads, and the runtime folders Hubzoid creates.
canonical_url: https://hubzoid.com/docs/reference/hub-files
last_updated: 2026-09-27
---

# Hub files

Reference for every file and folder a Hub can contain, the frontmatter keys each loader reads, and the runtime folders Hubzoid creates.

A Hub is one folder. Hubzoid reads the files below from it every time the Hub starts, and some of them on every call. Everything except the `.env` files and the runtime folders belongs in Git.

## Layout

```text
my-hub/
  AGENTS.md
  .env
  agents/
    researcher.md
  skills/
    weekly-summary/
      SKILL.md
  knowledge/
    policies.md
  tools_local/
    stock.py
  restricted/
    finance.py
    .env
  connectors/
    .mcp.json
  schedule/
    daily-reports.md
  workflows/
    stock_drift/
      main.py
    settings.yaml
  evals/
    refund-window.md
  identity/
    access.csv
    permissions.yaml
  raw_data/
  branding/
    logo.png
```

| Path | Required | Read by | Purpose |
| --- | --- | --- | --- |
| `AGENTS.md` | yes | every surface | The main agent's instructions and settings. |
| `.env` | no | every command that builds or serves the Hub | Model, keys and settings. See [configuration](https://hubzoid.com/docs/reference/configuration). |
| `agents/` | no | agent builder | Sub-agents, loaded as skills or run as delegates. |
| `skills/` | no | `list_skills`, `load_skill` | Playbooks the agent loads on demand. |
| `knowledge/` | no | `list_knowledge`, `read_knowledge` | Reference documents, read live from disk. |
| `tools_local/` | no | tool registry | Python tools. |
| `restricted/` | no | tool registry, access guard | Python tools that need a permission, plus their secrets in `restricted/.env`. |
| `connectors/.mcp.json` | no | MCP loader | External MCP servers the agent can call. |
| `schedule/` | no | scheduler | Markdown tasks run on a cron or by a webhook. |
| `workflows/` | no | workflow engine | Python code workflows and their settings. |
| `evals/` | no | `hubzoid eval`, scheduler | Behaviour checks. |
| `identity/` | no | access, inbound surfaces | Roster of people and their groups, and permission labels. |
| `raw_data/` | no | `grep_data`, `read_file`, `list_files` | Large source material to search rather than load. |
| `branding/` | no | `hubzoid run`, `hubzoid gateway` | Logo, favicon, splash and CSS for the web chat. |

### Folder name variants

Folder names match without regard to case, and these variants are accepted. When two variants exist in one Hub, the first alphabetically is used and a warning is logged.

| Canonical | Also accepted |
| --- | --- |
| `agents` | `agent` |
| `skills` | `skill` |
| `tools_local` | `tool_local`, `tools`, `local_tools` |
| `connectors` | `connector` |
| `schedule` | `schedules`, `scheduled` |
| `workflows` | `workflow` |
| `evals` | `eval`, `evaluations` |
| `raw_data` | `raw-data`, `rawdata` |
| `output` | `outputs` |

`knowledge`, `restricted` and `identity` have no variants.

## AGENTS.md

The main agent. The body is the system prompt, used verbatim. Frontmatter is optional, so a plain Markdown file works.

```markdown title="AGENTS.md"
---
name: ops-desk
description: Answers stock and order questions for the operations team.
model: claude-local/sonnet
suggestions:
  - Which items are below reorder level?
  - Summarize yesterday's orders
mcp_instructions: |
  Use read_knowledge for policy questions and stock_level for live counts.
---

You help the operations team of a home goods retailer with four stock locations.
Answer from knowledge files and tools. Never guess a stock count.
```

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `name` | string | the Hub folder name | The agent's name. Its slug is the model id in `/v1/models` unless `MODEL_LABEL` is set, and it is the web chat's display name unless `WEBUI_NAME` is set. |
| `description` | string | the first non-heading line of the body, up to 200 characters | Short summary. A gateway shows it in the model picker. |
| `model` | model id | none | The Hub's model when `.env` does not set `MODEL`. `MODEL` in `.env` wins. |
| `suggestions` | list of strings | none | Click-to-send prompts on the web chat's empty new-chat screen. |
| `mcp_instructions` | string | the body | Instructions sent to MCP clients when they connect, in place of the body. Use it when the body holds internal guidance or is long. |
| `auto_addendum` | boolean | `true` | `false` stops Hubzoid from appending its runtime section (knowledge index, skills index, tool guidance) to the instructions. |

A file with frontmatter but no body fails to load. `tools:` on the main agent has no effect: the main agent always has the full tool registry.

## Agent definitions: agents/

Each sub-agent is either a folder, `agents/<name>/AGENTS.md` (or the first `*.md` in the folder), or a single file, `agents/<name>.md`. Hidden entries are skipped.

```markdown title="agents/researcher.md"
---
name: researcher
description: When the user wants a researched brief on a supplier or product.
model: claude-local/opus
tools: [web_search, http_get, read_knowledge]
---

Research the question with web_search and http_get, then write a short brief with sources.
```

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `name` | string | the folder or file name | Identifier, and the skill name when loaded as a skill. |
| `description` | string | the first non-heading line of the body | When the main agent should use this sub-agent. Write it as a "when" sentence. |
| `model` | model id | none | Decides skill or delegate. See below. |
| `tools` | list of tool names | all tools | Delegates only. The tools the delegate may call. Unknown names are dropped with a warning. |

**Skill or delegate.** A sub-agent becomes a delegate only when its `model:` differs from the Hub's model on the same engine, for example `claude-local/opus` in a `claude-local` Hub, or a different LiteLLM id in a LiteLLM Hub. A delegate runs on its own model in its own context and returns its answer to the main agent, which stays in control. On the OpenAI Agents backend the main agent calls it as a tool named `handover_<name>`. On `claude-local` it runs as a Claude subagent. Every other sub-agent (no `model:`, the same model, or a different engine) is loaded inline as a skill, and its `tools:` list is ignored with a warning. A delegate whose model is missing its provider key falls back to a skill so the Hub still starts.

## Skills: skills/

Each skill is `skills/<name>/SKILL.md` (also `skill.md`, `Skill.md` or the first `*.md` in the folder) or a single file, `skills/<name>.md`.

```markdown title="skills/weekly-summary/SKILL.md"
---
name: weekly-summary
description: Three-bullet summary of the week's orders for the operations lead.
---

1. Call list_knowledge and read the reporting policy.
2. Pull the week's numbers with the stock and order tools.
3. Reply with three bullets and one risk to watch.
```

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `name` | string | the folder name (or file stem) when missing | Name passed to `load_skill`. |
| `description` | string | `Skill loaded from <file>.` when missing | Shown in the skills menu the agent sees. |

The agent sees each skill's name and description and loads the body with `load_skill` only when it needs it. Other frontmatter keys are ignored. Supporting files in a skill folder are not loaded automatically. Name them by their path from the Hub root in the skill body, and the agent reads them with `read_file`.

Name collisions resolve in this order: `skills/` first, then sub-agents loaded as skills, then Hubzoid's built-in `dashboard` skill (chat surfaces only). A skill file that cannot be parsed stops the Hub from building.

## Knowledge: knowledge/

Every `*.md` under `knowledge/`, including subfolders, is one document. Hidden files and `_index.md` are skipped.

```markdown title="knowledge/policies.md"
---
name: returns-policy
description: Return windows, exceptions and who approves refunds.
keywords: [returns, refunds, exchange]
---

Returns are accepted within 14 days with the original receipt.
```

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `name` | string | the file stem | Name passed to `read_knowledge`. |
| `description` | string | `Knowledge document: <stem>.` | Shown in the knowledge menu. |
| `keywords` | list of strings | none | Search hints. A single string is accepted. |

The knowledge tools read the folder live, so an edited or added document is visible on the next call without a restart. A file with broken frontmatter is skipped with a warning and the rest still load. Documents saved with the `remember` tool live in `knowledge/_learned/` with the name `learned/<topic>`. See [memory and history](https://hubzoid.com/docs/concepts/memory-and-history).

## Tools: tools\_local/ and restricted/

Every `*.py` file in these folders is imported when the Hub builds, and every module-level `FunctionTool` (a function decorated with `@function_tool` from the `agents` package) becomes a tool. Files whose names start with `_` are skipped.

- A `tools_local/` tool with the same name as a built-in tool replaces the built-in.
- A `restricted/<permission>.py` tool requires the permission named by the file stem. It wins over any tool of the same name and is guarded before every call.
- `restricted/.env` holds the secrets those tools read. File tools refuse every path under `restricted/`.

See [tools and connectors](https://hubzoid.com/docs/guides/tools-and-connectors) and [restrict tools](https://hubzoid.com/docs/guides/restrict-tools).

## Connectors: connectors/.mcp.json

External MCP servers, in the `mcpServers` shape. `connectors/mcp.json` (no leading dot) is read when `.mcp.json` is absent.

```json title="connectors/.mcp.json"
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GH_TOKEN}" }
    },
    "tickets": {
      "transport": "streamable-http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${TICKETS_TOKEN}" }
    }
  }
}
```

| Key | Applies to | Effect |
| --- | --- | --- |
| `command`, `args`, `env` | stdio servers | Process to start and its arguments and environment. |
| `url` | HTTP servers | Server endpoint. |
| `transport` | HTTP servers | `sse` or `streamable-http` (also written `http`). A server with a `url` and no `transport` uses SSE. A server with no `url` uses stdio. |
| `headers` | HTTP servers | Request headers, such as an authorization token. |
| `client_session_timeout_seconds` | HTTP servers | Per-call timeout on the OpenAI Agents backend. |

`${NAME}` in any string is replaced with that environment variable after `.env` is loaded. Names use uppercase letters, digits and underscores. With `HUBZOID_BROWSER=true`, a `playwright` server is added automatically, and an entry of your own with that name replaces it.

## Scheduled tasks: schedule/

One Markdown file per task. The file name (letters, digits, `.`, `_` and `-`, starting with a letter or digit) is the task name. The body is the agent's instructions, or optional notes for a `run:` script task.

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `schedule` | 5-field cron | none | When the task runs, in the machine's local time. |
| `on_webhook` | string or `true` | none | Run when events arrive at this webhook inbox instead of on a cron. `true` means `webhook`. Use `schedule` or `on_webhook`, not both. |
| `run` | string or list | none | Run a command instead of the agent. A string runs in a shell, a list runs as arguments without one. |
| `model` | model id | the Hub's model | Model for this task. |
| `timeout` | seconds | `1800` | Time limit per round. |
| `max_rounds` | integer | `10` | Fresh-context rounds per run. |
| `max_turns` | integer | `40` | Agent turns within one round. |
| `write` | path or list | none | Extra Hub paths the task may write, not committed. |
| `commit` | path or list | none | Hub paths the task may write and Hubzoid commits after the run. |
| `push` | boolean | `false` | Push the commit. Needs `commit`. |
| `enabled` | boolean | `true` | `false` keeps the file but stops it firing. |

The full behaviour, including rounds, catch-up and webhook inboxes, is in [markdown tasks](https://hubzoid.com/docs/guides/markdown-tasks).

## Workflows: workflows/

Each code workflow lives in `workflows/<name>/`. Every `*.py` file there whose name does not start with `_` is imported, and each function decorated with `@workflow` is registered under its function name. `hubzoid new workflow` scaffolds one. The API is in the [workflow API reference](https://hubzoid.com/docs/reference/workflow-api).

`workflows/settings.yaml` is optional. Workflows read any top-level key with `hub.setting("<key>")`, and two keys configure the engine:

```yaml title="workflows/settings.yaml"
agent_max_attempts: 1         # tries for a failed hub.call_agent step. 1 means no retry.
max_concurrent_workflows: 2   # hub-wide cap on code workflows running at once. Unset means no cap.
stock_drift:
  threshold_pct: 5
```

## Evals: evals/

One Markdown file per case. The file name is the case name. Files starting with `_` or `.` are skipped. Frontmatter keys are `schedule`, `tags`, `expect_tools`, `forbid_tools`, `contains`, `not_contains`, `timeout`, `threshold` and `enabled`, and any other key is an error. The body holds a `## Prompt` section and an optional `## Criteria` section. See [evals](https://hubzoid.com/docs/guides/evals).

## Identity: identity/

### identity/access.csv

A roster of people. The header row names the columns. `phone`, `email` and `groups` have meaning to Hubzoid, and any other column is kept as context on the person's record.

```text title="identity/access.csv"
phone,email,groups,center
919800000001,meera@example.com,coordinator,north
919800000002,arjun@example.com,coordinator;finance,south
```

- `phone` identifies WhatsApp and Telegram senders. Formatting is normalized, so a prettified number matches.
- `email` links the row to the person's web chat and MCP identity, and is lowercased.
- `groups` are separated by `;` or `,`. Rows that share an email combine their groups.

The file reloads when it changes on disk, with no restart. An unreadable or half-written file resolves to no groups, never to old ones, so save it by writing a new file and renaming it into place. On WhatsApp and Telegram the roster is the allowlist: a sender whose number is not listed is refused. For web chat and MCP callers the roster only adds groups.

### identity/access.py

A function-backed roster for live lookups, such as a CRM. It wins over `access.csv` when both exist.

```python title="identity/access.py"
def resolve(surface: str, handle: str) -> dict | None:
    """Return {"email": ..., "groups": [...]} for a phone or chat handle, or None."""
    ...

def groups_for_email(email: str) -> list[str]:
    """Optional. Groups for a web chat or MCP caller, looked up by email."""
    ...
```

`groups_for_email` is optional, and without it the file never receives email lookups. Results of `groups_for_email` are cached for 60 seconds. An exception in either function denies rather than failing the request.

### identity/permissions.yaml

Labels for the permissions the Console shows. The Console lists `use_hub`, `manage_access` and one permission per `restricted/*.py` file, and this file adds text to them without running any tool code.

```yaml title="identity/permissions.yaml"
finance:
  label: Finance reports
  description: Read margins and receivables from the accounting system.
  sensitive: true
```

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `label` | string | `Use this agent` for `use_hub`, `Manage access` for `manage_access`, otherwise the name in title case | Name shown in the Console. |
| `description` | string | empty | Explanation shown with the permission. |
| `sensitive` | boolean | `false` | Tags the permission as sensitive in the Console, and the review step shows a warning before it is granted. |

Access concepts are covered in [identity and access](https://hubzoid.com/docs/concepts/identity-and-access).

## Branding: branding/

Files copied into the web chat on every start. Names match without regard to case.

| File | Used for |
| --- | --- |
| `favicon.svg`, `favicon.png`, `favicon.ico` | Brand mark in the tab and sidebar. |
| `logo.svg`, `logo.png` | Used as `favicon.svg` or `favicon.png` when that file is absent. |
| `favicon-dark.png`, `favicon-96x96.png`, `apple-touch-icon.png`, `logo.png`, `splash.png`, `splash-dark.png`, `web-app-manifest-192x192.png`, `web-app-manifest-512x512.png` | Individual slots. Any PNG slot you leave out is filled from `favicon.png`. |
| `custom.css` | Replaces Hubzoid's baseline stylesheet for the web chat. |

When `branding/` holds any file, Open WebUI's own name suffix and branding are replaced, unless `HUBZOID_KEEP_OWUI_SUFFIX=true`. In a gateway, a Hub's `logo` or `favicon` raster (PNG, WebP, JPEG or GIF) becomes its avatar in the model picker.

> **Warning: Open WebUI branding terms**
>
> Open WebUI's license allows removing its branding only in specific cases, such as deployments with 50 or fewer users in a 30-day period or with an Open WebUI enterprise license. Read the [Open WebUI license](https://docs.openwebui.com/license/) before you add files to `branding/`, and set `HUBZOID_KEEP_OWUI_SUFFIX=true` to keep Open WebUI's branding.

## Runtime folders

Hubzoid creates these while it runs. Keep them out of Git and include them in backups with `hubzoid backup`.

| Path | Contents |
| --- | --- |
| `.hubzoid/hub.db` | The Hub's own tables, when SQLite is used. |
| `.hubzoid/dbos.db` | The workflow engine's run history, when SQLite is used. |
| `.hubzoid/schedule-state.json` | Fire times and last results of markdown tasks and scheduled evals. |
| `.hubzoid/schedule/<task>/` | A markdown task's scratch folder, always writable by that task. |
| `.hubzoid/chats/<chat>/uploads/`, `.hubzoid/chats/<chat>/artifacts/` | Files people attached, and files the agent saved with `write_artifact`. |
| `.hubzoid/evals/` | One JSON file per eval run. The last 50 are kept. |
| `.hubzoid/artifact_secret` | Secret that signs download links (mode `0600`). |
| `.hubzoid/backups/` | Access snapshots written by `hubzoid access migrate --apply`. |
| `.openwebui-data/` | The web chat's database, uploads and log for a single Hub. |
| `.inbound/webhooks/<name>/` | Webhook events waiting for a task. Handled events move to `.processed/`. |
| `.inbound/` | Delivery dedup records and Telegram bindings. |
| `output/<session>/` | Fallback folder for tool output when no chat is in scope, such as `hubzoid test`. |

## Next steps

- [Project structure](https://hubzoid.com/docs/getting-started/project-structure): A guided tour of the same folders with a working example.
- [Built-in tools](https://hubzoid.com/docs/reference/built-in-tools): The tools every Hub has before you add your own.
- [Markdown tasks](https://hubzoid.com/docs/guides/markdown-tasks): Schedule instructions and scripts with schedule/\*.md.
- [Bring an existing agent](https://hubzoid.com/docs/guides/existing-agents): Map a Claude Code or similar project into these files.
