Facets
A facet is a Markdown-defined persona that controls prompt framing, tool access, skill access, model pinning, and session color. The frontmatter declares the facet settings, and the body supplies the system-prompt framing the model works under. Where Instructions Live helps you choose between a facet, a skill, and an AGENTS.md file; Prompting and Input covers day-to-day switching.
Switching facets changes the system prompt and the available tools while keeping
the same conversation. The shipped plan and execute facets work together:
the model researches and writes a plan in plan, then handoff_plan moves the
work into execute, which gives the model its edit and shell tools. The shipped
orchestrate facet coordinates complex work through subagents while the parent
model retains responsibility for dependencies, verification, and synthesis.
Each facet supplies its own system prompt, and that prompt sits ahead of the entire conversation in every request. Switching facets therefore causes a cache miss on the next call: the conversation arrives under a prompt prefix the provider has not cached, so the call gets no cache discount unless the provider still holds a live cache entry for the facet you switched to. That first call rebuilds the cache from the new prefix, and later calls return to the cached rate.
Where facets live
Section titled “Where facets live”A facet file goes in .polytoken/facets/ in your project, or in
$XDG_CONFIG_HOME/polytoken/facets/ (default: ~/.config/polytoken/facets/)
for facets you want everywhere. The filename is the facet’s name: scribe.md
defines a facet named scribe, and the name key in the frontmatter must
match the filename, or Polytoken rejects the file.
Facets override by replacement, not by merge. Polytoken searches shipped and
global facets first, then each daemon.discovery.extra_facets_dirs entry in
config order, then each --facets-dir directory supplied when you create a
session, and finally project .polytoken/facets/. A later directory replaces a
facet with the same name. Project facets therefore take precedence over session
extras, session extras over config extras, and config extras over global and
shipped facets. Polytoken skips missing directories. The
daemon.follow_symlinks_for_configs setting controls whether Polytoken follows
symlinks while loading definitions. exec --facets-dir adds facets for that
invocation only. The daemon.discovery.extra_facets_dirs setting is available
in configuration files, not in the interactive configurator. A facet defined
under the config facets key takes precedence over every filesystem definition
with the same name. A custom plan replaces the shipped plan; it does not
extend the shipped definition.
The body is a template
Section titled “The body is a template”The body below the frontmatter is a template; see Templating for the syntax. Polytoken renders the facet body into the facet’s complete system-prompt framing and adds no base prompt of its own.
In most facets you will want Polytoken’s shipped base prompt under your own
framing. Include it with transclude at the top of the body:
{{ transclude("polytoken://system_prompts/facet.md") }}
Your job is to help the user with their documents.The shipped plan, execute, and orchestrate facets all start this way, then
add their own lines. The base prompt carries tool-use guidance and other
behavior the model depends on. Leaving the transclude out means writing that
guidance yourself.
@skill: references in a facet template
Section titled “@skill: references in a facet template”A facet template can include @skill:<name>. Polytoken resolves the reference when the facet activates and applies that facet’s own polytoken.skills_allow and polytoken.skills_deny lists. A broken, denied, or polytoken.disable_model_invocation: true reference fails activation. The HTTP facet route returns 422, and the switch_facet tool returns an error. During startup or rewind, Polytoken restores a fallback facet instead. Repeated switches do not stack duplicate skill reminders, and compaction does not resurrect skills from a facet you left.
Customizing the shipped plan facet
Section titled “Customizing the shipped plan facet”The shipped plan facet investigates with read-only shell access for repository
inspection but with project editing tools withheld, writes a handoff plan, runs
the built-in plan-reviewer, and asks you before moving work into execute.
The plan facet exposes shell_exec so the model can run repository-state
commands that built-in tools cannot cover, such as git status, git log, and
gh. The facet prompt steers the model toward grep, glob, and file_read
for file inspection, and both the prompt and the autonomous classifier prohibit
any shell command that modifies files, the working tree, or system state. The
reviewer checks the plan against your project override when
project_vars.plan_facet.plan_spec_override is set, or against Polytoken’s
default plan shape otherwise.
Replace the whole facet with .polytoken/facets/plan.md when you want to change
that behavior, the tools the facet exposes, or the prompt outside the plan
format. The plan facet does not track its own planning work with todos, because
todos persist into the execute facet after a handoff; the todo tools stay
available for when you ask for them by name.
To keep the shipped behavior and change only the handoff-plan format, set a
project variable for the shipped plan facet. The variable replaces the default
plan format rather than adding to it, and plan-reviewer checks plans against
that same override, so write the complete plan-format instructions you want
both the model and the reviewer to follow. See
Template Reference
for the exact key and YAML shape.
A small facet
Section titled “A small facet”This example defines a scribe facet in .polytoken/facets/scribe.md, giving
the model a documents-assistant framing and Notion tools:
---name: scribepolytoken: tools: [mcp__notion] color: "#7c3aed"---{{ transclude("polytoken://system_prompts/facet.md") }}
Your job is to help the user with their documents. You have full access to theirNotion workspace through its tools. Work from what the user asks for.mcp__notion grants every tool from the Notion MCP server. To grant the server
but exclude one tool, add a tools_deny:
tools: [mcp__notion] tools_deny: [mcp__notion__delete_page]The polytoken.tools list is the complete set of tools the facet may use.
Connected MCP servers and installed extensions add nothing by themselves. An
MCP tool appears only through tag!ALL_MCP, mcp__<server>, or the full
mcp__<server>__<tool> name, and an extension tool appears only through its
literal name in the list.
Three helper tools are Polytoken’s own, not MCP or extension tools, and the
polytoken.tools list does not control them. Polytoken adds tool_search
when any model in the configuration sets tool_loading to native_deferred,
or when the configuration includes an MCP server. mcp_list_resources and
mcp_read_resource appear when the configuration includes an MCP server. A
facet does not need to declare these helpers. Naming a helper in
polytoken.tools_deny removes it.
Denying tool_search under a deferred-tools configuration fails closed. A
facet (or a permission rule) that removes tool_search while the governing
model defers tools leaves the deferred catalog stranded — no tool can be
revealed. Polytoken refuses such compositions up front: a facet switch into
that state is rejected with the reason and remedy, a subagent spawn with that
projection fails with a typed error, and any turn that would shape such a
request ends immediately with an actionable error instead of silently hiding
the catalog. The remedy is to keep tool_search available (remove it from
polytoken.tools_deny or the denying permission rule) or switch the
governing model to eager tool loading.
Frontmatter keys
Section titled “Frontmatter keys”Conventional keys like name sit at the top level. Every Polytoken-specific key
is polytoken.<key>. The table below lists the keys a facet can use.
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | required | The facet’s name. Must match the filename. |
polytoken.model | string | facet-bound model group for the shipped execute, plan, and orchestrate facets; the active model otherwise | Pins the facet to a model or a model group (@mg:<group>). A facet pinned to a group rides the group’s failover chain. Without it, the facet uses the active model. The shipped execute, plan, and orchestrate facets pin their facet-bound groups. See Pinning a model group. |
polytoken.fallback_models | list | none | Models to fall back to when the primary is unavailable. |
polytoken.tools | list | none | The complete set of tools the facet exposes; the list is a ceiling, not a floor. Accepts literal names (including the full mcp__<server>__<tool> name for an MCP tool and the extension tool’s literal name for an extension tool), mcp__<server> for all of a server’s tools, tag!ALL for every core tool, and tag!ALL_MCP for every MCP tool. Tools from connected MCP servers and installed extensions appear only when the facet declares them through one of these forms. Polytoken also adds its own helpers regardless of the list: tool_search when any model sets tool_loading to native_deferred or the configuration includes an MCP server, mcp_list_resources and mcp_read_resource when the configuration includes an MCP server, and polytoken.tools_deny removes any helper it names. |
polytoken.tools_deny | list | none | Tools to remove from the granted set. Literal names only, using the full mcp__<server>__<tool> name for an MCP tool. |
polytoken.undeferred_tools | list | none | The granted tools whose full definitions appear up front rather than through tool search. Literal names only. |
polytoken.skills_allow | list | allow all | The skills the facet permits, as names or tag!<name> groups. Leaving it out means no restriction, so every skill is allowed. |
polytoken.skills_deny | list | none | The skills the facet blocks. Deny wins over allow. |
polytoken.color | string | none | The facet’s accent color, as #RRGGBB. Sets both light and dark. |
polytoken.color_light | string | none | The accent color for light themes. Overrides color. |
polytoken.color_dark | string | none | The accent color for dark themes. Overrides color. |
polytoken.autonomous_hint | string | none | Guidance for the autonomous permission classifier: in Autonomous mode, Polytoken adds this text to the classifier’s context for each tool call made in this facet, steering the allow, ask, and deny decision. See what the classifier sees for the complete input context. |
polytoken.compaction_hint | string | none | Guidance Polytoken appends to the compaction summary prompt when this facet is active, naming the context the summary should preserve for this facet’s work. |
polytoken.facet_transitions.<target>.allowed | boolean | open | Whether this facet may switch to <target>. With no transitions declared, every switch is allowed. |
polytoken.facet_transitions.<target>.condition | string | none | When set, switching to <target> first asks you to confirm, showing this text. |
When a facet omits all three color fields, Polytoken generates a deterministic accent from the facet name for both light and dark themes. The two generated colors share the same hue but differ in lightness so each reads against its background.
Pinning a model group
Section titled “Pinning a model group”polytoken.model accepts a model-group reference (@mg:<group>), not just a
single model name. A project-local facet that pins a group rides the group’s
failover chain: the facet activates the group’s current candidate, and when a
candidate fails over (rate limits, terminal provider errors) the session
follows the group, including across detach and resume. Put the facet file in
your project’s .polytoken/facets/ directory:
---name: reviewpolytoken: model: "@mg:primary" tools: - file_read---Review the current diff.With [modelgroups] primary = ["model-a", "model-b"] in your configuration,
switching to the review facet routes through the group. The shipped facets
(execute, plan, orchestrate) pin their own facet-bound groups:
polytoken:execute_facet, polytoken:plan_facet, and
polytoken:orchestrate_facet, each resolving to the full tier’s default model
(see the [modelgroups] section of the configuration reference). A
handoff_plan approval names a target facet, and a shipped facet carries its
own model pin, so switching to a shipped facet (by handoff or switch_facet)
selects that group’s current candidate instead of keeping the session’s
previous model. Each facet-bound group is a reserved model-group name you can
override: authoring that name in [modelgroups] replaces the shipped value,
so the facet then follows your selection. When the bound group has no usable
selection, for example when no full-tier default resolves, the switch fails
with an error naming the unresolved reference rather than silently keeping
the previous model. An explicit --model passed at startup bypasses the
group and pins that model directly. polytoken new follows the same rule:
with polytoken new --model X --facet Y, Polytoken applies the model, then
the facet, then re-applies the model you named, and the session starts on X.
Tool and skill access notes
Section titled “Tool and skill access notes”switch_facet is a regular tool. List it in polytoken.tools to let a facet
switch to another facet. The shipped execute facet includes it; plan leaves
it out, so the model cannot leave planning on its own and uses handoff_plan to
transition the work to you.
handoff_plan submits the active plan for your approval; you can implement it
in a fresh context or in the current one, or cancel. The model names the target
facet when it calls handoff_plan; the facet argument defaults to execute,
and the model can name orchestrate there as well. Polytoken has no config
key for this default, so when you want a handoff to orchestrate, tell the
model in the plan facet prompt or in your request. An approved handoff that
names a shipped facet switches through that facet’s group, so the session
continues on the group’s current candidate.
Polytoken checks the plan files in the session plan directory before approval.
Polytoken refuses the handoff when the directory contains another plan that has
not already been approved or when a previously approved plan changed. The
refusal does not switch facets or clear the context. Set confirm_plan_path to
the active plan filename or its full path when you have reviewed the newer or
changed files and want Polytoken to adopt the current directory contents.
The shipped plan facet runs the built-in plan-reviewer before handing off;
the review is a recommendation, not a requirement. You decide at the approval
step whether to proceed, and you can skip review entirely. The reviewer evaluates the current
saved plan, so its findings correspond to the version you are approving.
Leaving polytoken.skills_allow out means no restriction, so the facet gets every
skill. Once you write a non-empty list, though, it is a commitment: if your
patterns match no skills, through a misspelled name or an empty tag! group,
Polytoken fails the facet’s skill access closed rather than reverting to allow-all: the facet runs with no skills.
The tag!ALL and tag!ALL_MCP shorthands belong in polytoken.tools. The
lowercase tag!<name> form is for skills only, in polytoken.skills_allow and
polytoken.skills_deny, and Polytoken rejects it in a tool list.
Goal tools and unresolved tool names
Section titled “Goal tools and unresolved tool names”The four goal lifecycle tools are propose_goal, read_goal,
complete_goal, and block_goal. When your facet lists any of them in
polytoken.tools, the model can call them directly. This holds for every
model, including models that load tools on demand, where a facet tool would
otherwise wait in a catalog until the model searches for it. Subagents never
receive these tools; they belong to the main session.
A tool name your facet declares can fail to resolve. A typo, an extension
tool that is not installed, or an MCP server that is not connected all leave
the name without a backing tool. Polytoken warns instead of rejecting the
facet, because membership depends on what is loaded at runtime. You see the
warning in the daemon log when Polytoken starts and when you reload its
configuration. polytoken validate facet prints a note for names it cannot
recognize statically. The model receives a one-time reminder naming the
unresolved tools the first time it would be affected. Fix the spelling or
load the missing tool; a facet that declares an unresolvable name keeps
working, and calls to the missing tool fail.
To see exactly which tools a facet resolves to, request the
GET /tools/effective route with the daemon’s bearer token. It returns the
active facet’s tool plan, or any named facet’s plan when you pass
?facet=<name>. The plan lists the tools the model can call directly, the
tools waiting in the deferred catalog, the tools hidden by permission rules,
and any unresolved names. A named facet’s plan reflects the model a switch
to that facet would activate, so a facet that pins a different model shows
that model’s tool loading. The request changes nothing: it never switches
the facet or the model.
Loading changes
Section titled “Loading changes”Polytoken loads facets when it starts and when you reload its configuration. Editing a facet file does not take effect until then.