- name
- customize
- description
- Create, configure, and maintain custom agent profiles and author new skills via the `repl` tool. Use when the user wants to create an agent profile, build a custom agent, modify agent capabilities, attach or detach skills/connectors on a profile, author a skill, or inspect which connectors and tools are available. Also use whenever you need the `host.agents.*` or `host.skills.*` Python SDK.
- license
- Apache-2.0
# Customize
Build and maintain **agent profiles** and **skills** programmatically via the
`repl` tool using `host.agents.*` and `host.skills.*`.
A **profile** is a named bundle that shapes how an agent behaves:
- **`system_prompt`** — the profile's **identity**. This is the opening of the
agent's system prompt; it REPLACES the generic "You are Claude Science" base identity.
Write it in second person, lead with `You are {display_name}, ...`, state what
the agent specializes in and what it does NOT do. Everything else (tool-usage
rules, working-style bullets, scope guardrail) is inherited automatically —
don't restate it.
- **`display_name` / `description` / `icon_key` / `color_key`** — picker metadata.
- **`skill_names`** (optional restriction) — by default a profile sees the
**full live skill catalog** via `search_skills` / `skill(...)`, same as the
main agent. Pass an explicit list ONLY to deliberately restrict it; `[]` creates
a zero-skill specialist. **Restricting skills also restricts connectors** —
a single `unrestricted` flag governs both; passing `skill_names` flips the
profile to curated mode and starts it with **zero** connectors (see next).
- **Connector access** — an **unrestricted** profile (the default) reaches
**every connector** (bundled + custom + authorized directory), same as the
main agent; use `detach_connector` to subtract specific ones. A **curated**
profile (one created with an explicit `skill_names` list, or flipped via
`{"unrestricted": False}`) starts with **no connectors** — reach is exactly
what you `attach_connector`.
- **`excludedTools`** — per-tool blocklist applied *after* connectors resolve.
Use to strip specific high-risk or irrelevant tools from an otherwise-useful
connector. **Per-connector, not a profile field** — set via
`attach_connector(..., include_tools_pattern=/exclude_tools_pattern=)`; the
profile's `excludedTools` in `list()` is the read-only aggregation across all
its attached connectors. Patterns match the connector's **bare** tool names
(e.g. `'^list_marts$'`, as returned by `list_connectors(name)['tools']`); the
aggregated `excludedTools` entries are stored fully-qualified as
`mcp_<connector>_<tool>` since the list spans every attached connector.
---
## Python SDK
All calls run via the **`repl` tool** (see "Runs via the `repl` tool"
below). Return values are plain dicts/lists; errors raise `RuntimeError`
with a `host.agents.*:` / `host.skills.*:` prefix.
### `host.agents`
```python
host.agents.list()
# → [{"name", "displayName", "description", "source", "enabled",
# "systemPrompt", "iconKey", "colorKey",
# "skillNames": ["skill", ...], "connectors": ["name", ...],
# "excludedTools": [...]}, ...]
# (Return-dict keys are camelCase — wire shape, not kwarg names.)
# `connectors` is a list of connector names (strings, same as skillNames) —
# pass one to attach_connector/detach_connector or list_connectors(name).
host.agents.create(name, display_name, description,
system_prompt="", skill_names=None)
# name: 2–32 chars, UPPERCASE letters / digits / underscores only —
# e.g. "RNASEQ_REVIEWER". Lowercase / dashes are rejected.
# display_name is the human-friendly picker label.
# skill_names controls catalog visibility:
# - leave it unset → the profile sees the FULL live skill catalog, same as
# the main agent — skills published later appear automatically. It also
# gets every connector the main agent has, resolved dynamically — new
# connectors added later appear automatically too. This is the default;
# don't pass a list unless you mean to restrict.
# - pass a list (including []) → restricts `search_skills`/`skill(...)` to
# EXACTLY those names. [] creates a zero-skill specialist.
# → the stored profile record (same shape as one list() entry)
host.agents.update(name, patch)
# patch: dict of fields to change — any of display_name, description,
# system_prompt, skill_names, unrestricted, icon_key, color_key
# (camelCase also OK).
# patch["skill_names"] is an EXACT REPLACE of the whole skill list and flips
# the profile to restricted mode — anything you omit is DETACHED. To add or
# remove a few skills, use attach_skill / detach_skill — they work on both
# restricted AND unrestricted profiles without changing the mode.
#
# If you do need a full-list replace, CHECK cur["unrestricted"] FIRST: on an
# unrestricted profile cur["skillNames"] is the lossy disk-cache view (not
# the full live catalog), so `cur["skillNames"] + ["x"]` would permanently
# freeze the profile to that partial list. The safe pattern:
# cur = [a for a in host.agents.list() if a["name"] == name][0]
# if cur["unrestricted"]:
# host.agents.attach_skill(name, "new-skill") # stays unrestricted
# else:
# host.agents.update(name,
# {"skill_names": cur["skillNames"] + ["new-skill"]})
# (return-dict keys are camelCase — read "skillNames", write "skill_names")
# patch["unrestricted"] = True → back to the full live catalog + all
# connectors (undoes a skill_names restriction).
# → updated profile record
# (excludedTools is NOT a patch field — it's per-connector; use
# attach_connector's include_tools_pattern/exclude_tools_pattern below.)
host.agents.switch(name)
# Ask to continue THIS conversation as `name`. Shows the user an approval
# card; on Allow, the switch takes effect on their NEXT message (the current
# turn finishes as the current profile). On decline, tell the user they can
# select the profile from the session config popover on any new conversation.
# → {"switched": True, "name", "displayName"}
host.agents.delete(name)
# → {"deleted": name}
host.agents.attach_skill(name, skill)
host.agents.detach_skill(name, skill)
# → updated profile record
host.agents.attach_connector(name, connector,
include_tools_pattern=None,
exclude_tools_pattern=None)
host.agents.detach_connector(name, connector)
# → updated profile record. Omit both patterns on a fresh attach to expose
# every tool the connector offers; re-attaching an already-attached
# connector without patterns preserves its existing exclusion list (pass
# include_tools_pattern='.*' to clear it). Patterns match the connector's
# BARE tool names (as returned by list_connectors(name)['tools'], e.g.
# '^list_marts$'); the resulting excludedTools entries are stored
# fully-qualified as mcp_<connector>_<tool>.
host.agents.list_connectors(connector_name=None)
# no arg → [{"name", "displayName", "source", "description",
# "authState", "attachedAgents": [...]}, ...]
# with connector_name → single dict with an extra
# "tools": [{"name", "description"}, ...]
```
### `host.skills`
```python
host.skills.list()
# → [{"name", "origin", "description"}, ...]
# origin: "anthropic" (bundled, read-only — fork under a new name),
# "organization"/"personal" (editable), "draft" (local, unpublished)
host.skills.read(name, path="SKILL.md")
# → {"name", "path", "content": "..."}
host.skills.edit(name, path, content, old_string=None)
# old_string=None → create `path` with `content` (fails if the file already
# exists — read it, then edit with a non-empty old_string)
# old_string=str → str_replace the single exact match (rejected unless it
# matches exactly once — add surrounding context if needed)
# → {"action", "path", "draft_path", "note"}
host.skills.publish(name, overwrite=False)
# publish takes NO content args — write SKILL.md via .edit() first.
# → {"status": "published", "skill_id", "name", "note"}
host.skills.delete(name)
# draft → removes local dir; org/personal → unpublishes + removes local
# cache; anthropic bundled → protected.
# → {"deleted": name} (plus "unpublished": True for published skills)
```
### Runs via the `repl` tool
`host.agents.*` / `host.skills.*` execute in the **control-plane
kernel** — a separate Python process from your `python` cells, reached via
the **`repl` tool** (not the `python` tool). It shares your workspace
directory (cwd) but **not** memory, so variables from `python` cells aren't
visible there and vice-versa. To hand results across, write to a file —
same pattern as Python↔R:
```python
# repl tool
import json, os
os.makedirs("handoff", exist_ok=True)
profiles = host.agents.list()
json.dump(profiles, open("handoff/agents.json", "w"))
```
```python
# python tool
import json, pandas as pd
profiles = json.load(open("handoff/agents.json"))
pd.DataFrame(profiles)[["name", "source", "enabled"]]
```
---
## Workflow: scope → draft → review → create
**Do not call `generate_plan` for profile CRUD.** This is a single
scope→draft→confirm loop; `ask_user` (step 4) is the review gate. A plan
adds a second approval that duplicates the `ask_user` confirmation and
drags in step-status bookkeeping that fights this workflow.
**User approval.** Most `host.agents.*` / `host.skills.*` calls apply
immediately — `create`/`update` (including unrestricted),
`attach_*`/`detach_*`, and `skills.publish`/`skills.edit` are pre-approved
at session start and the cell does NOT pause. An approval card (cell
pauses, resumes automatically on Allow — you don't retry) appears only
for the calls that **hand off identity or free a granted name**:
- `host.agents.switch(name)` — per-target; Allow covers this name only
- `host.agents.update(name, {"name": ...})` (rename) — per-target
- `host.agents.delete(name)` / `host.skills.delete(name)` — **one card
per project**: the first delete shows a card; "Allow for this project"
covers every subsequent delete in this project, including bulk
teardown. Do NOT tell the user they'll see N cards for N deletes.
The name is read from the runtime call for `switch` and rename, so both
literal and variable names work (`host.agents.switch("FOO")` or
`host.agents.switch(name_var)`); keep the `ask_user` review step
so the click is a quick confirm, not a surprise. After the call returns,
read back (`host.agents.list()`) to confirm the actual state — don't
narrate an expected card.
### Reading existing profiles
Call `host.agents.list()` to see the user's current profiles so you don't
duplicate an existing agent. The main agent's bundled profile is protected —
it cannot be renamed or deleted.
### 1. Scope first
What is this agent *for*? A profile should have one job. Pick an UPPER_SNAKE
name (`RNASEQ_REVIEWER`, not `RNA-seq review helper`). Use `ask_user` if the
name or scope is unclear — profiles are user-visible in the picker.
### 2. Write the identity
`system_prompt` is the agent's opening paragraph — it replaces the base
identity, it's not an addendum. Lead with `You are {display_name}.` State the
specialization and the boundaries ("You handle X, Y, Z. You do not handle
..."). Keep it under ~200 words; the heavy how-to lives in skills, not the
prompt.
### 3. Ask: full access or a subset?
Before creating, `ask_user` whether this profile should have **full access**
(the live skill catalog and every connector — same reach as the main agent;
new skills and connectors appear automatically) or a **restricted subset**
(a fixed list you'll curate together). Don't infer this from the role
description — a narrowly-described specialist may still want full reach,
and a broadly-described one may want a tight loadout. Pair this with the
name/prompt review in step 4 so it's one round-trip.
### 4. Review with the user
Show the proposed name, display name, description, `system_prompt`, and the
user's full-vs-subset choice from step 3. `ask_user` to confirm before
writing. If they chose a subset, list the proposed skills/connectors here.
### 5. Create
Call `host.agents.create(name, display_name, description, system_prompt=...)`.
If the user chose **full access**, leave `skill_names` unset. If they chose a
**subset**, pass `skill_names=[...]` with the agreed list — **this flips the
profile to curated mode and starts it with zero connectors**, so attach the
agreed connectors after create as in step 6. For edits to an existing profile,
use `host.agents.update(name, {...})` with targeted fields; prefer
`host.agents.attach_skill(...)` / `host.agents.detach_skill(...)` and
`host.agents.attach_connector(...)` / `host.agents.detach_connector(...)`
over wholesale `skill_names` replacement so you don't clobber the user's own
edits.
After the profile exists, **offer to switch to it**:
`host.agents.switch(name)`. The user sees an approval card; on Allow, this
conversation continues as the new specialist from their next message. If they
decline, tell them they can select it from the session config popover on any
new conversation.
### 6. Restricting the loadout (when the user chose a subset)
If the user chose a subset in step 3, curate after create:
- **Skills**: `host.agents.update(name, {"skill_names": [...]})` with the
exact list, or `detach_skill` one at a time. Check `host.skills.list()`
for available names. On `update`, `skill_names` is an exact replace — never
send a partial list to "add"; fetch the current `skillNames` (camelCase in
the return dict), modify, send back.
- **Connectors**: a curated profile (created with an explicit `skill_names`
list, or flipped via `{"unrestricted": False}`) starts with **NO
connectors** — reach is exactly what you attach. Call
`host.agents.list_connectors()` to see every available connector (bundled +
directory + user-added MCP) with its auth state, then
`host.agents.attach_connector(name, connector_name)` for **each** connector
the user agreed to keep. Use `include_tools_pattern=`/`exclude_tools_pattern=`
on the attach call to scope which tools the profile gets (omit both to
expose every tool; re-attaching without patterns preserves the existing
exclusion list — pass `include_tools_pattern='.*'` to clear). A connector
with `authState` other than `"authorized"` or `"not-required"` must be
connected via the Connectors panel before it can be attached.
`detach_connector` only removes an explicit attachment; on a curated
Ver en GitHub