Extending kin — tools, kinds, and profiles¶
How to add a new tool to the kin harness. This page is the user-facing
mirror of the canonical walkthrough in
src/kin/harness/CLAUDE.md
— the source-tree version stays the single source of truth; this page
exists so a contributor landing on docs/ doesn't have to know to
navigate into src/kin/harness/CLAUDE.md first.
Adding a tool¶
Tools live in src/kin/harness/tools/ (kin.harness.tools). The
Tool base class + ToolContext are in tools/registry.py.
To add a tool:
-
Subclass
Tooland set the class attrs:name— what the model calls ("web_fetch","shell", etc.).description— short, what the tool does. The model reads this; keep it tight.parameters— JSON Schema for the args. Strict-decode opt-in viaKIN_STRICT_TOOLS(see Models & providers).kind— the risk class the gate reads (READ/EDIT/SHELL/META; egress tools reuseMCPorNETWORK, the browser has its ownBROWSER, the read-onlyoutposttool has its ownOUTPOST— see Permissionspermissions.py). This drives the mode → permission decision. A NEW kind must land in BOTH mode-policy dicts + the planning-freeze tuple in one change (seemodes.pyrow insrc/kin/harness/CLAUDE.md) — the alternative fails open.
-
Implement
async run(args, ctx) → str(or alist[ContentBlock]for multimodal results — seetools/_media.py; the loop normalizes both viacoerce_content). Use thectxhelpers:ctx.emit(event)— post anEventto the UI / journal.ctx.progress(text)— live-tail text in the UI (for streaming).ctx.parent_tool_call_id— MUST be threaded onto anytool_streamevent you emit, or the UI can't group the stream under the right tool call.
-
Untrusted output (web / file external content) must go through
frame_untrustedintools/_util.pybefore entering history. SeeREFERENCE.md§ "Untrusted content". -
Numeric args should use
coerce_int(intools/_util.py) — don't bare-int()model args; junk → default, clamps. Boolean args usecoerce_bool— never raw truthiness: a hallucinated"false"string is Python-truthy but a falsy token under the contract (seeREFERENCE.mdcritical invariant — the contract is load-bearing for the permission gate'sNetwork allow-listtags). -
Register the tool in
tools/__init__.py::default_registry(). Most tools register unconditionally; thesshtool is gated onssh_hostsconfig and thesearch_workspacetool onsearch_enabled(v3 §4 + §6 respectively). Thebrowsertool is gated onKIN_BROWSER=1(orbrowser.enabled = true) and the optionalplaywrightextra actually being importable — the model is never shown a tool the box can't back. Theoutpost/outpost-sendpair follows the same default-off-by-absence pattern, registering only once bothoutpost_urlandoutpost_tokenresolve. -
Add coverage under
tests/test_harness/(andtests/test_web.pyif it's a web tool). The harness suite is the commit gate — a tool without a verify assertion is a silent regression waiting to happen.
Process / subprocess tools (currently just shell.py) need
start_new_session=True for process groups, and cleanup in finally.
See REFERENCE.md § "Process & subprocess".
Adding a permission kind¶
The mode → permission gate lives in src/kin/harness/permissions.py
+ src/kin/harness/modes.py. There are eight built-in kinds: READ,
EDIT, SHELL, META, MCP, NETWORK, BROWSER, OUTPOST. To add
a ninth:
- Add the constant to
permissions.py(e.g.EGRESS = "egress"). - Map it in BOTH mode-policy dicts —
AUTO.policyandSTRICT.policyinmodes.py. The default is fail-OPEN (policy.get(kind, perm.ALLOW)inMode.decide) — a new kind that isn't in both dicts will silently ALLOW everywhere. - Add it to
loop._planning_freezesif the kind should be denied while planning (NETWORKis,MCPis,BROWSERis). All three lists must change together —Mode.decide's unmapped-kind fall-through is the entire reason the "both dicts must change together" invariant is load-bearing. - Add a kind-row entry to
docs/guide/tools.mdwith a one-line summary of the new tool's risk class + mode behavior. - Add a verify assertion under
tests/test_harness/that asserts the new kind is a key in BOTHAUTO.policyandSTRICT.policy, and in_planning_freezesif applicable. The structural test is the load-bearing backstop.
Adding a subagent profile¶
Subagent profiles (the .md files behind task <profile> and the
/critique / /security-review / /deep-research bundled
workflows) live in src/kin/harness/defaults/agents/ (bundled) +
.kin/agents/ + .claude/agents/ (user + project, project wins).
-
Create
<name>.mdwith frontmatter:--- name: <name> description: <one-line, model-readable> max-turns: 20 # optional — default 100, clamped to 1-1000 tools: # optional — omit entirely for "all tools available" - read_file - grep - shell --- <system-prompt body — same shape as the harness `compose_system`>tools:is a YAML list of exact tool names (not aread|write|editshorthand) — see the names registered intools/__init__.py::default_registry(). There's nomodel:frontmatter key: a profile always inherits the parent's model unless the dispatcher passes a per-callmodelargument totask(...). -
For a profile that ships with kin (an exemplar), drop the file in
src/kin/harness/defaults/agents/. For a user-only profile, drop it in.kin/agents/<name>.mdor.claude/agents/<name>.md. -
For a critic-style profile (read-only, rubric-based), follow the
critic-code.mdtemplate — seesrc/kin/harness/defaults/agents/critic-code.mdfor the rubric + severity-sorted output contract. -
If the profile is dispatched by a bundled command (like
/critique), pair it with adefaults/commands/<name>.mdthat routes the body askind: workflowand dispatches the agent subagent via theagent()primitive. -
Add coverage —
tests/test_harness/test_skills_agents.py/test_subagents.pyfor profile resolution + tool-allow-list enforcement;tests/test_workflow.pyfor the bundled-command wiring.
See src/kin/harness/CLAUDE.md agents.py row for the discovery
mechanics (5-root walk + project-shadows-bundled).
Where to read next¶
src/kin/harness/CLAUDE.md— the canonical per-file harness guide (the source of truth for everything on this page).docs/concepts/permissions.md— the conceptual frame for the mode → permission gate.docs/guide/tools.md— the tool catalog (what each existing tool does, the kind each carries).REFERENCE.md— every gotcha, footgun, security invariant, and postmortem. Read before touching the permission gate, the planning freeze, or any tool that touches the filesystem / network.