Skip to content

Agent Profiles

An agent profile is a mission-catalog role — the same role file format used by agent teams — made selectable as an agent in bnerd web and the TUI, alongside the built-in chat, code, and pa agents. Pick go-cli-dev, security-reviewer, or any other role your mission repo defines, and you get that role's tool allowlist, its own system prompt, and (optionally) its own permission rules — without spinning up a full team.

This page covers the role schema as it applies to a standalone chat profile, where roles are loaded from, and how permission rules compose when you use one. For the team-recipe side of role files (spawning a role as a teammate, disallowedTools, skills, plan-mode) see Team Recipes.

What becomes a profile

Every role file bnerd's agent-teams catalog loads is registered as a chat profile — there is no separate opt-in. If your mission repo (or .bnerd/agents/) defines it as a role, it shows up under its slug in both the bnerd web mode selector and the TUI's :agent list.

Layered sources

Roles are loaded from three locations, in precedence order (a role earlier in this list wins a slug collision — the loader still records the collision so you can spot it):

  1. Mission repo<mission repo>/orchestrator/roles/*.md
  2. Global~/.bnerd/agents/*.md
  3. Project<workdir>/.bnerd/agents/*.md

A mission repo is optional: with none configured (and no .bnerd/agents/ directories anywhere), no agent profiles are registered and both bnerd web and the TUI behave exactly as they always have — chat, code, and pa only.

Every way a role can fail to become a profile is reported on stderr, and bnerd web/the TUI still boot with whatever profiles they do have:

  • A role file that doesn't parse — a malformed permission: block, broken frontmatter, or no name: at all — is skipped, and the file path and the reason are printed. bnerd team list/describe/run and the TUI's team launch print the same warnings for the same files.
  • A role that can't be registered — its slug collides with a builtin (chat, code, pa), with a permission class name (read, write, destructive), or with a delegated-agent name (sub-agent, explore-agent, kube-agent) — is skipped with its own warning.

The one thing that is not survivable is naming a mode that doesn't exist: see Using an agent profile.

Role frontmatter

A role file is Markdown with YAML frontmatter; the body becomes the role's system prompt (appended after the standard chat prompt). See Team Recipes → Role frontmatter fields for name, description, tools, disallowedTools, model, skills, permissionMode, and color. Two more fields exist on every role: reviewer: true opts a role into satisfying task gates, and subagents: true grants the read-only fan-out tools. Both only matter once the role is used as a teammate — they are inert for a standalone chat profile. Two things worth calling out specifically for the chat-profile use case:

  • tools lists Claude Code tool names (Read, Grep, Bash, …), which are translated to bnerd's own tool names at spawn — see Safety Model → Per-role scoped tool registries for the translation step and which names are dropped. Team-coordination names (SendMessage, TaskCreate, TaskUpdate, TaskList, TaskGet) translate to team_* tools that only exist inside an actual team run — as a standalone chat profile those entries are simply absent from the registry, since there is no team context to bind them to.
  • subagents: true is ignored by a standalone chat profile, in bnerd web and the TUI alike. The sub_agent/explore_agent tools emit stream events neither surface's single-conversation chat renders, so a role that opts in would be offering the model a tool whose output is silently dropped. The flag keeps its full meaning when the same role is spawned as a teammate — by bnerd team run, or by a team started from the TUI conversation — whose runtime does drive those tools. In the TUI a teammate's subagents surface as nested rows under its row in the live team panel, and its tool events render in that teammate's own thread once you focus into it — see Team Recipes. This restriction is per-surface, not permanent: it lifts when the chat surfaces grow subagent-event handling.

What a role profile gets beyond its tools: list

The tools: allowlist governs the platform and local tool surface — filesystem, git, shell, and the cloud-API tools. Two things are registered outside it, deliberately:

  • The read-only scratchpad utilities, which back the model's own note-taking within a conversation. They carry no access to anything a role isn't already allowed to touch.
  • Any skills scoped to the role's mode name. A skill scoped to go-cli-dev activates when you run in the go-cli-dev profile — that scoping is the opt-in, so requiring the role to also list the skill's tools would just mean stating it twice.

Everything else a role can reach comes from its allowlist.

Role skills are force-enabled

A role's own skills: frontmatter field (see Team Recipes → Role frontmatter fields) is a separate, explicit mechanism from the mode-scoped activation above. Every name in skills: is force-activated — loaded and switched on regardless of that skill's own applies_to restriction. This is deliberate: applies_to exists to keep auto-suggested skills (keyword matches, the self-serve skill tool's directory) relevant to the surface they're offered on; it isn't a veto over an operator naming a skill by hand in a role file. A skill authored with applies_to: [code, chat] can still be force-loaded into a role named go-cli-dev, even though go-cli-dev never appears in that list.

Force-loaded skills are injected into the profile's system prompt once, at build time — there is no per-round refresh for them the way the profile's ordinary chat session gets from SetSkillsFn. A role's skills: set is static for the life of the session.

A name in skills: that matches no loaded skill (typo, or a skill defined only in a scope this profile's workdir/mission repo doesn't reach) doesn't fail the role — it's dropped with a warning to stderr at startup, naming the role and the unresolved skill(s). The same warning mechanism reports unknown skill names when a role spawns as a team teammate instead of a profile (bnerd team run, or a team started from the TUI conversation) — see Team Recipes.

Note that this profile's own chat session also keeps the ordinary self-serve skill tool (see Rules & Skills), operating on a second, separate registry scoped to the working directory — so a go-cli-dev profile session can both arrive with its role's force-loaded skills already active AND load any other applicable skill on demand mid- conversation. A role spawned as an actual team teammate does not get that self-serve tool; see Rules & Skills → Where the skill tool is (and isn't) available.

The permission: block

A role's frontmatter can carry an optional permission: block with the same shape as an --ai-permissions profile file:

---
name: locked-down-reviewer
description: Read-only review, no shell access at all.
tools: [Read, Grep, Glob, Bash]
permission:
  allow: ["shell_exec(git diff*)", "shell_exec(git log*)"]
  deny: ["shell_exec(git push*)", "shell_exec"]
---
You review diffs. Read and reason; never modify anything.

Entries follow the same tool_name / tool_name(pattern) syntax and per-entry validation as a profile file. Unlike the general frontmatter convention used for tools:/skills:/etc. (which silently drops entries it doesn't understand), a malformed permission: entry — an unterminated pattern, a non-string list item — fails the role's parse outright, naming the file and role. A deny entry is a safety boundary; silently dropping one because of a typo is not acceptable, so the role simply doesn't load until it's fixed.

A block present but with no entries in any of the three lists is treated the same as no block at all.

Avoid class-wide denies in a role's permission block

A role's permission: rules match against tool names and safety classes (read, write, destructive) exactly like a profile file's rules do — see How the engine decides. That means a class-wide entry like deny: ["read"] doesn't just narrow the role's own file/shell tools — it also matches the always-on team coordination tools (team_send_message, team_task_list, team_task_get, …), which are all classified SafetyRead and registered after the role's permission overlay when the role runs as a teammate. A role with deny: ["read"] in its permission: block can spawn as a teammate and then be unable to send messages or read the task list. Write role permission rules against specific tool names (or shell_exec(pattern)) instead of the class strings, unless denying an entire class is genuinely the intent.

Using an agent profile

bnerd web — launch directly into a role:

bnerd web --mode go-cli-dev

Or start in any mode and switch live from the mode selector in the chat header — the dropdown is driven by the same list of registered profiles, so every loaded role appears there next to chat, code, and pa. Switching carries the live conversation — history, scratchpad, and the session record — into the newly selected role; see Web UI → switching modes for the carry and refusal semantics.

An unknown --mode is fatal

If the name you pass doesn't resolve to a profile, bnerd web refuses to start and lists the modes it does have. It does not fall back to chat: a role is picked precisely for its tool allowlist and permission block, so silently substituting the most permissive builtin would widen the session behind your back — the opposite of what a typo should do. Switching modes live from the dropdown rejects an unknown name the same way, leaving the current mode running.

If the role you named should exist, check stderr: a role file that failed to parse is skipped with a warning printed just above this error.

TUI — there's no launch flag for an arbitrary role (only bnerd pa has a dedicated shortcut, into the builtin pa agent); switch onto one from inside the TUI's one conversation with :agent <role-slug> (bare :agent lists every registered profile, roles included, marking the active one). See AI Assistant → Switching agents for what carries over and when the switch is refused. Unlike bnerd web's fatal --mode, naming an unknown role to :agent just prints the available names and leaves the conversation on its current agent.

Each agent/mode shares the one conversation now, so switching to a role and back to chat doesn't lose either thread's context — this matches how chat/code/pa already behave, in both bnerd web and the TUI.

MCP server: no chat-profile concept

The MCP server (bnerd mcp) exposes a raw tool registry gated by safety mode (read-only/non-destructive/full), not a conversational agent — there's no chat session to select a profile for, so agent profiles don't apply there.

How permission rules compose

The --ai-mode floor, a role's own permission: block, and a session-level --ai-permissions profile all stack, in this order — each layer is applied on top of whatever is already live, never replacing it:

  1. The active --ai-mode preset — the floor. read-only, non-destructive, or full, same as any other chat session. A role can never see or run a tool the mode's safety-class floor forbids, regardless of what its own tools: allowlist or permission: block says — the mode check happens independently at tool Register/Execute time, so nothing below can widen past it.
  2. The role's own permission: block, if it has one, is applied on top of the mode preset when the role's tools are registered (at agent build time — when the session for that mode is first created, not when the profile is registered at startup). Its allow/ask/deny entries win over the preset wherever they match (last-match-wins).
  3. An operator --ai-permissions profile, if one is configured for the bnerd web session, is composed on top of whatever is already live — the mode preset plus the role's own rules from step 2, if any. The operator's profile rules are appended last, so under last-match-wins the operator's profile is the final word in both directions: an operator deny overrides a role's allow, and an operator allow equally overrides a role's deny, wherever the two overlap. A role rule the operator's profile never mentions stays in effect — a locked-down-reviewer role that allows shell_exec(git diff*) keeps that allowance even with an operator profile loaded, unless the profile itself says otherwise. This is the same "operator has the last word" composition --ai-permissions already used for the built-in chat/code/pa modes (which have no permission block of their own, so there was nothing to compose against there) — a role's own rules now get the identical treatment.

    Delegation is a separate, deny-only mechanism

    The three layers above compose within one session, where the operator's profile can widen as well as narrow. Handing privilege down to a subagent or teammate never widens: only the operator's profile denies are inherited, tracked separately from the session's live engine and independent of whatever the role's permission: block contributed to it. Subagent deny inheritance therefore behaves the same whether or not the role has a permission: block — see Deny inheritance.

Deny inheritance into subagents and teammates

Anything a session delegates to inherits the operator's own --ai-permissions profile deny rules — not the role's own permission: denies, and not the mode preset's catch-all deny. Delegation only ever narrows.

For a standalone chat profile this covers the built-in sub_agent, explore_agent, and kube_agent tools where the profile has them. For a role spawned as a teammate it covers the teammate itself and, transitively, the subagents that teammate spawns when its role sets subagents: true: the operator's denies follow the whole chain down, not just the first hop. See Permission Rules for the full mechanics and the reasoning for why it's specifically the profile's denies, not the mode's own preset, that get inherited.

See also

  • Team Recipes — the full role/team file schema, for using the same roles inside an actual team run.
  • Safety Model — the per-role tool registry build order and the Claude Code → bnerd tool-name translation.
  • Permission Rules — the permission engine, profile files, and deny inheritance in full.