Skip to content

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:

  1. Subclass Tool and 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 via KIN_STRICT_TOOLS (see Models & providers).
    • kind — the risk class the gate reads (READ / EDIT / SHELL / META; egress tools reuse MCP or NETWORK, the browser has its own BROWSER, the read-only outpost tool has its own OUTPOST — see Permissions
    • permissions.py). This drives the mode → permission decision. A NEW kind must land in BOTH mode-policy dicts + the planning-freeze tuple in one change (see modes.py row in src/kin/harness/CLAUDE.md) — the alternative fails open.
  2. Implement async run(args, ctx) → str (or a list[ContentBlock] for multimodal results — see tools/_media.py; the loop normalizes both via coerce_content). Use the ctx helpers:

    • ctx.emit(event) — post an Event to the UI / journal.
    • ctx.progress(text) — live-tail text in the UI (for streaming).
    • ctx.parent_tool_call_idMUST be threaded onto any tool_stream event you emit, or the UI can't group the stream under the right tool call.
  3. Untrusted output (web / file external content) must go through frame_untrusted in tools/_util.py before entering history. See REFERENCE.md § "Untrusted content".

  4. Numeric args should use coerce_int (in tools/_util.py) — don't bare-int() model args; junk → default, clamps. Boolean args use coerce_bool — never raw truthiness: a hallucinated "false" string is Python-truthy but a falsy token under the contract (see REFERENCE.md critical invariant — the contract is load-bearing for the permission gate's Network allow-list tags).

  5. Register the tool in tools/__init__.py::default_registry(). Most tools register unconditionally; the ssh tool is gated on ssh_hosts config and the search_workspace tool on search_enabled (v3 §4 + §6 respectively). The browser tool is gated on KIN_BROWSER=1 (or browser.enabled = true) and the optional playwright extra actually being importable — the model is never shown a tool the box can't back. The outpost / outpost-send pair follows the same default-off-by-absence pattern, registering only once both outpost_url and outpost_token resolve.

  6. Add coverage under tests/test_harness/ (and tests/test_web.py if 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:

  1. Add the constant to permissions.py (e.g. EGRESS = "egress").
  2. Map it in BOTH mode-policy dictsAUTO.policy and STRICT.policy in modes.py. The default is fail-OPEN (policy.get(kind, perm.ALLOW) in Mode.decide) — a new kind that isn't in both dicts will silently ALLOW everywhere.
  3. Add it to loop._planning_freezes if the kind should be denied while planning (NETWORK is, MCP is, BROWSER is). 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.
  4. Add a kind-row entry to docs/guide/tools.md with a one-line summary of the new tool's risk class + mode behavior.
  5. Add a verify assertion under tests/test_harness/ that asserts the new kind is a key in BOTH AUTO.policy and STRICT.policy, and in _planning_freezes if 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).

  1. Create <name>.md with 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 a read|write|edit shorthand) — see the names registered in tools/__init__.py::default_registry(). There's no model: frontmatter key: a profile always inherits the parent's model unless the dispatcher passes a per-call model argument to task(...).

  2. 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>.md or .claude/agents/<name>.md.

  3. For a critic-style profile (read-only, rubric-based), follow the critic-code.md template — see src/kin/harness/defaults/agents/critic-code.md for the rubric + severity-sorted output contract.

  4. If the profile is dispatched by a bundled command (like /critique), pair it with a defaults/commands/<name>.md that routes the body as kind: workflow and dispatches the agent subagent via the agent() primitive.

  5. Add coveragetests/test_harness/test_skills_agents.py / test_subagents.py for profile resolution + tool-allow-list enforcement; tests/test_workflow.py for the bundled-command wiring.

See src/kin/harness/CLAUDE.md agents.py row for the discovery mechanics (5-root walk + project-shadows-bundled).

  • 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.