Skip to main content
ClaudeWave
Skill354 repo starsupdated today

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.

Install in Claude Code
Copy
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-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_