Skip to main content
ClaudeWave
Skill349 repo starsupdated today

opt-in-tool-registration

The opt-in-tool-registration skill provides a structured pattern for conditionally registering tools in Claude agents behind feature flags, ensuring tools remain invisible until explicitly enabled. Use this when adding new tool sets that should default to disabled, requiring developers to implement separate tool maps, conditional merging into agent configs, execute-level guards that check feature enablement before argument validation, and gating tests to verify disabled behavior.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/zaxbysauce/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-registration
Then start a new Claude Code session; the skill loads automatically.

SKILL.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_