opt-in-tool-registration
This Claude Code skill defines a pattern for registering tools behind optional feature flags that remain hidden by default. Use it when adding new tool sets that require explicit user opt-in, ensuring tools are unavailable until enabled through configuration and properly guarded with checks before argument validation occurs.
git clone --depth 1 https://github.com/ZaxbyHub/opencode-swarm /tmp/opt-in-tool-registration && cp -r /tmp/opt-in-tool-registration/.opencode/skills/generated/opt-in-tool-registration ~/.claude/skills/opt-in-tool-registrationSKILL.md
# Opt-In Tool Registration Pattern
Activates when adding new tools that are gated behind a config flag (disabled by
default). Use this pattern to ensure tools are invisible when the feature is off
and properly guarded when on.
## When to Use
- Adding a new set of tools behind a feature flag (e.g., `external_skills`,
`memory`, `curation_enabled`)
- Registering tools that should not appear in agent menus until explicitly
enabled
- Any tool group where the default state is "off" and the user must opt in
## Pattern Overview
The opt-in tool registration pattern has four components:
1. **Separate tool map** — isolated constant in `constants.ts`
2. **Conditional merge** — merge into agent configs only when enabled
3. **Execute guard** — config check BEFORE argument validation in `execute()`
4. **Gating tests** — verify absent/present/disabled-message behavior
## Step 1 — Create the Tool Files
Create tool files in `src/tools/` following the standard `createSwarmTool`
pattern. Each tool's `execute()` function must:
1. Load the relevant config section
2. Check if the feature is enabled
3. Return the disabled message if not enabled
4. **Only then** validate arguments
```typescript
// src/tools/my-feature-tool.ts
export const myFeatureTool = createSwarmTool({
name: "my_feature_tool",
description: "...",
parameters: { ... },
execute: async (args, ctx) => {
// 1. Load config — MUST come first
// (example: replace with actual config loading for your feature)
const config = resolveConfig(ctx.directory).my_feature;
if (!config?.enabled) {
return {
content: [{ type: "text", text: "My feature is not enabled. ..." }],
};
}
// 2. Validate arguments — AFTER enabled check
const parsed = mySchema.safeParse(args);
if (!parsed.success) {
return { content: [{ type: "text", text: `Invalid args: ${parsed.error.message}` }] };
}
// 3. Business logic
...
},
});
```
**Critical**: The enabled check MUST precede argument validation. Otherwise,
calling with empty args while disabled returns validation errors instead of the
disabled message. This is the most common bug in opt-in tool implementations.
## Step 2 — Create the Agent Tool Map
Add a separate constant in `src/config/constants.ts`:
```typescript
// DO NOT add to AGENT_TOOL_MAP directly
export const MY_FEATURE_AGENT_TOOL_MAP: Record<string, string[]> = {
architect: ["my_feature_tool", ...],
coder: [...],
reviewer: [...],
// Only include agents that need the tools
};
```
Export from `constants.ts`. `TOOL_NAMES` is derived automatically from
`TOOL_METADATA` in `src/tools/tool-metadata.ts` — do not edit
`src/tools/tool-names.ts` directly (it is a re-export facade).
## Step 3 — Conditional Merge in Agent Config Builder
In the agent config builder (typically `src/agents/index.ts` or similar),
conditionally merge the opt-in map:
```typescript
import { MY_FEATURE_AGENT_TOOL_MAP } from "./constants";
function buildAgentConfigs(config: PluginConfig) {
// ... base config building ...
// Conditional merge
if (config.my_feature?.enabled) {
for (const [role, tools] of Object.entries(MY_FEATURE_AGENT_TOOL_MAP)) {
if (agentConfigs[role]) {
agentConfigs[role].tools = [...agentConfigs[role].tools, ...tools];
}
}
}
return agentConfigs;
}
```
## Step 4 — Register in Tool Metadata and Manifest
The registration chain has two compile-checked files:
1. **`src/tools/tool-metadata.ts`** — Add a `TOOL_METADATA` entry with the tool's
name, description, and default agents. This is the single source of truth for
tool registration metadata. `ToolName`, `TOOL_NAMES`, and `TOOL_NAME_SET` are
derived automatically.
2. **`src/tools/manifest.ts`** — Add a lazy thunk handler (`() => tool`) for the
tool. This file is compile-checked against `TOOL_METADATA`: a missing entry in
either file is a compile error.
Registration is always present regardless of enabled state. The tool is
registered but non-functional when disabled (returns the disabled message).
## Step 5 — Write Gating Tests
Three test categories are mandatory:
### 5a. Tools absent when disabled
```typescript
test("my_feature tools not in agent config when disabled", () => {
const config = { my_feature: { enabled: false } };
const agents = buildAgentConfigs(config);
for (const agent of Object.values(agents)) {
expect(agent.tools).not.toContain("my_feature_tool");
}
});
```
### 5b. Tools present when enabled
```typescript
test("my_feature tools in agent config when enabled", () => {
const config = { my_feature: { enabled: true } };
const agents = buildAgentConfigs(config);
expect(agents.architect.tools).toContain("my_feature_tool");
});
```
### 5c. Disabled message before validation errors
```typescript
test("disabled tool returns disabled message, not validation error", async () => {
const config = { my_feature: { enabled: false } };
const result = await myFeatureTool.execute({}, mockCtx(config));
expect(result.content[0].text).toContain("not enabled");
expect(result.content[0].text).not.toContain("Invalid args");
});
```
## Step 6 — Export and Wire
Complete the registration chain:
1. Add a `TOOL_METADATA` entry in `src/tools/tool-metadata.ts` (name, description, agents)
2. Add a lazy thunk handler in `src/tools/manifest.ts` (compile-checked against metadata)
3. Add the tool name to the opt-in map in `src/config/constants.ts`
4. Add to the conditional merge in agent config builder (typically `src/agents/index.ts`)
5. Add to help/documentation surfaces
6. Write tests covering all 5a/5b/5c categories
Run `tests/unit/config/*.test.ts` and `/swarm doctor tools` after any changes.
## Common Failures
### Enabled check after validation
Symptom: Calling tool with empty args while disabled returns "Invalid args"
instead of "Feature not enabled".
Fix: Move the config load + enabled check to the top of `execute()`.
### Tools in base AGENT_>
Run a rigorous, quote-grounded codebase review or security/QA/accessibility/performance/AI-slop/enhancement audit. Use for full-repo or large-subsystem review reports; not for normal implementation. Performs Phase 0 inventory, selected exhaustive tracks with non-diluting depth, coverage closure, reviewer/critic validation, and writes .swarm/review-v8 artifacts without modifying source files.
>
>
Use when asked to trace, investigate, root-cause, plan, fix, close, or prepare a PR for a GitHub issue or bug report. Runs an evidence-first issue workflow: GitHub intake, reproduction, reasoning-guided localization, no-gap fix planning, independent critic review, user approval gate, implementation, tests, and PR-ready closure.
>
>