# Custom Operator Tools (`[[tool]]`): Design Exploration (https://jackin.tailrocks.com/reference/research/operator-surface/host-affordances/custom-operator-tools-design/)



**Status**: Design exploration for [Custom operator tools](/roadmap/custom-operator-tools/) (Phase 2, [Agent Orchestration Program](/reference/research/agent-orchestration/program-research/)) — not yet implemented

## Problem [#problem]

jackin❯ already has pieces that custom tools should reuse: in-container `jackin-exec`, the credential picker, and MCP execution plumbing. Missing is the operator-configured `[[tool]]` surface: reserved-key validation, substitutions from workspace/session/tag state, console rendering, output capture, and clear host-vs-container execution semantics.

Different teams want different operator-side actions: "run my repo's lint script against the agent's worktree", "open my org's issue tracker URL for the active issue", "snapshot the agent's data dir before a risky test", "slack the team that the agent finished". Today the only way to express these is to wrap jackin❯ in shell aliases or external scripts.

multicode addresses this with a `[[tool]]` array: each entry is a hotkey that runs a command (or prompts for input and runs a command) when pressed in the TUI. Reserved keys (`q`, `a`, `d`, `s`) are off-limits; everything else is operator-defined.

## Why It Matters [#why-it-matters]

* It's the difference between jackin❯ being a *fixed* operator surface and an *extensible* one. The latter is what teams need to standardize on jackin❯ instead of building custom CLI wrappers.
* It pairs cleanly with [agent runtime status](/roadmap/agent-runtime-status/) and [tag protocol](/roadmap/agent-tag-protocol/): the captured tags become substitution arguments for the tool command, so a tool can be as simple as "open the issue URL the agent declared".
* It uses the same handler-resolution machinery as the [operator handler system](/roadmap/operator-handler-system/) — different config surface, same primitives.

## Inspiration in multicode [#inspiration-in-multicode]

**Sources**:

* No dedicated README section
* Config — [`config.toml` `[[tool]]` array](https://github.com/graemerocher/multicode/blob/main/config.toml) (the example only shows one tool — `bash` on `b` — but the schema is the same)
* Source — [`tui/src/app.rs`](https://github.com/graemerocher/multicode/blob/main/tui/src/app.rs) (tool firing, reserved-key validation)

```toml
[[tool]]
type = "exec"
name = "Run lint"
key = "l"
exec = "bun run lint"

[[tool]]
type = "prompt"
name = "Slack the team"
key = "k"
prompt = "What did the agent ship?"
```

multicode has two `type` values:

* `exec` — runs the command verbatim (shell-words parsed). The TUI invokes the tool with cwd = workspace dir. Output displayed in a modal.
* `prompt` — opens an input modal asking the operator's question, then runs an associated command with the operator's answer interpolated. Times out at 300 seconds.

Reserved keys are documented; collisions error at config load.

## Recommended Shape [#recommended-shape]

Same shape, with two extensions: argument substitution from captured agent tags, and per-role scoping so a Rust role gets `cargo` shortcuts and a JS role gets `bun` shortcuts.

### Config [#config]

```toml
# ~/.config/jackin/config.toml

[[tool]]
key = "l"
name = "Run lint"
exec = "cargo clippy"
# Optional scope filter — only show this tool when the workspace is
# loaded against a matching role. Same selector grammar as
# [docker.mounts] scope.
scope = "the-architect"

[[tool]]
key = "i"
name = "Open emitted issue"
exec = "open {issue}"
# {issue} substitutes the agent's most-recent <jackin:issue> URL.
# Available substitutions: {repo}, {issue}, {pr}, {link}, {workspace},
# {instance}, {workdir}.

[[tool]]
key = "n"
name = "Note for ops log"
type = "prompt"
prompt = "What happened?"
exec = "echo {input} >> ~/jackin-ops.log"
```

`type` defaults to `"exec"`. `scope` defaults to no filter (tool always visible).

### Reserved keys [#reserved-keys]

jackin❯ existing reserved keystrokes (the navigation set in the console, plus quit/exile/eject) are off-limits. Tool config validation errors at config load if a custom tool collides with a reserved key.

### Substitution [#substitution]

When the tool fires, jackin❯ resolves `{X}` placeholders against the selected workspace's runtime state:

* `{instance}` — container name
* `{workspace}` — workspace name
* `{workdir}` — primary mount destination inside the container
* `{host_workdir}` — the materialized host path (worktree if isolated)
* `{repo}`, `{issue}`, `{pr}`, `{link}` — most-recent value from the [tag protocol](/roadmap/agent-tag-protocol/) index
* `{input}` — only for `type = "prompt"`; the operator's response

Missing placeholders error at fire time with a helpful message ("no `<jackin:issue>` recorded for this instance"); they don't silently expand to empty.

### Where commands run [#where-commands-run]

Tools run on the **operator host**, not inside the container. They get the operator's environment plus a few injected variables matching the substitution set. If a team needs to run *inside* the container, they can `docker exec` from the tool command — explicit and intentional.

## Scope (V1) [#scope-v1]

* `[[tool]]` array at the operator-config level.
* `type = "exec"` and `type = "prompt"` (default `exec`).
* `scope` filter on role selector.
* Substitution for `{instance}`, `{workspace}`, `{workdir}`, `{host_workdir}`, `{input}`, plus tag-protocol values once that ships.
* Reserved-key collision detection at config load.
* 300-second prompt timeout (matches multicode).
* Tool firing routes through the [operator handler system](/roadmap/operator-handler-system/) for command resolution and output rendering.

## Defer [#defer]

* Per-workspace tool overrides. Operator-config-level only in V1.
* Streaming output for long-running tools. V1 captures and displays on completion, like multicode.
* Tool authoring helpers (operator-facing CLI for "scaffold a tool config from this command"). Defer until friction surfaces.
* Tool emission of `<jackin:*>` tags (so a tool can update the per-instance tag index). Interesting; defer.
* Tool composability (one tool invokes another). Out of scope.

## Open Questions [#open-questions]

* **Where does the tool's stdout go?** A modal that overlays the console matches multicode but interrupts flow. A scrolling log panel is friendlier but requires the [console resource panel](/roadmap/console-resource-panel/) infrastructure. Recommended default: *modal for V1*, log panel later.
* **Do tools work outside the console?** A `jackin tool run <key>` CLI command would be useful for scripting. Recommended: *yes, V1.1*; V1 ships console-only.
* **Substitution scope when no agent is loaded.** If the operator picks a workspace but no agent has run yet, `{instance}` and `{workdir}` are unset. Recommended: *fire-time error*, never silent empty.

## Related Files [#related-files]

* New module (e.g. `src/console/tools.rs`) — tool config + substitution + firing
* <RepoFile path="crates/jackin/src/cli/config.rs">crates/jackin/src/cli/config.rs</RepoFile> — `[[tool]]` parsing
* <RepoFile path="crates/jackin/src/console/tui.rs">crates/jackin/src/console/tui.rs</RepoFile> — keybinding registration and modal rendering
* The handler system module — output rendering routes through there
