Tutorial 3 — Plugin authoring¶
Build a Claude-Code-format plugin from scratch — a skill, a command, a sub-agent, and a Python hook — then install it into swarm and verify every surface registers.
Time: ~20 minutes.
End state: a plugin directory you can git push and anyone can install. Works in Claude Code and swarm identically.
1. Scaffold the directory¶
CC-format plugins are a directory with a specific layout:
my-first-plugin/
├── .claude-plugin/
│ └── plugin.json # required manifest
├── skills/ # optional
│ └── bfsi-glossary/
│ └── SKILL.md
├── commands/ # optional
│ └── explain-pan.md
├── agents/ # optional
│ └── kyc-validator.md
├── hooks/ # optional
│ ├── hooks.json
│ └── pan_mask.py
└── README.md # recommended
from pathlib import Path
root = Path('/tmp/my-first-plugin')
for d in ['.claude-plugin', 'skills/bfsi-glossary', 'commands', 'agents', 'hooks']:
(root / d).mkdir(parents=True, exist_ok=True)
print(root)
2. Write the manifest¶
plugin.json is required. Only name is strictly required; everything else is optional but recommended.
import json
(root / '.claude-plugin' / 'plugin.json').write_text(json.dumps({
'name': 'bfsi-helper',
'description': 'BFSI-specific helpers: glossary skill, KYC validator agent, PAN masking hook.',
'version': '0.1.0',
'author': {'name': 'Your Name', 'email': 'you@yourorg.com'},
'homepage': 'https://github.com/yourorg/bfsi-helper',
'license': 'Apache-2.0',
'keywords': ['bfsi', 'compliance', 'kyc'],
}, indent=2))
print((root / '.claude-plugin' / 'plugin.json').read_text())
3. Write a skill (skills/bfsi-glossary/SKILL.md)¶
Skills are keyword-matched procedural knowledge. When an agent's context contains the skill's trigger words, the skill body gets injected into the agent's system prompt for that turn.
(root / 'skills' / 'bfsi-glossary' / 'SKILL.md').write_text('''---
description: Indian BFSI acronym + regulator glossary. Inject when the conversation mentions BFSI terminology.
triggers:
- PAN
- Aadhaar
- CIBIL
- KYC
- NACH
- RBI
- DPDPA
---
# BFSI terminology reference
## Identifiers
- **PAN** — Permanent Account Number. 10-char alphanumeric. Format: `AAAAA9999A`. Issued by Income Tax Dept.
- **Aadhaar** — 12-digit unique ID. NEVER log or persist in cleartext. Always mask last 4 digits only: `XXXX-XXXX-1234`.
- **CIN** — Corporate Identification Number. 21-char format for companies.
## Credit bureaus
- **CIBIL** — TransUnion CIBIL. Score range 300-900; default-level cutoff typically ~700.
- **Experian India** — alternative bureau; same score range.
- **CRIF High Mark**, **Equifax India** — additional bureaus.
## Regulators
- **RBI** — Reserve Bank of India. Banking + NBFC regulator.
- **SEBI** — Securities and Exchange Board of India. Capital markets.
- **IRDAI** — Insurance Regulatory & Development Authority.
- **MeitY** — Ministry of Electronics and IT. Administers DPDPA.
## Compliance frameworks
- **RBI FREE-AI** — RBI\'s seven-Sutra AI governance guidelines.
- **DPDPA** — Digital Personal Data Protection Act, 2023.
- **Master Direction on IT Outsourcing** — 2023 directive; data localisation.
## Payment rails
- **NACH** — National Automated Clearing House. Recurring payments.
- **UPI** — Unified Payments Interface. Real-time retail payments.
- **IMPS**, **NEFT**, **RTGS** — legacy transfer rails.
''')
print('skill written')
4. Write a slash command (commands/explain-pan.md)¶
Commands are explicit-invocation templates with $ARGUMENTS substitution.
(root / 'commands' / 'explain-pan.md').write_text('''---
description: Explain what a PAN number is, validate a given PAN format, and flag common mistakes.
model: fast
---
Explain Indian PAN (Permanent Account Number) to a non-Indian engineer:
1. What PAN is
2. 10-character format (5 letters + 4 digits + 1 letter)
3. Common gotchas (leading zeros, look-alike characters)
4. Validate this PAN if provided: $ARGUMENTS
Keep response under 300 words. Use bullet points.
''')
print('command written')
5. Write a sub-agent (agents/kyc-validator.md)¶
Plugin agents are auto-namespaced on install as plugin-bfsi-helper::kyc-validator so they can't shadow built-ins.
(root / 'agents' / 'kyc-validator.md').write_text('''---
name: kyc-validator
description: Validates KYC payloads — PAN format, Aadhaar length, address completeness, OTP expiry. Does not phone home; pure format validation.
model: fast
tools:
- execute_python
---
You are a KYC payload validator for Indian BFSI.
Given a JSON payload, check:
1. `pan` matches regex `^[A-Z]{5}[0-9]{4}[A-Z]$`
2. `aadhaar` is 12 digits (accept with or without spaces; normalise)
3. `address` has: street, city, state, pincode (6 digits)
4. `otp_expiry` if present is an ISO datetime in the future
Return a JSON report: `{valid: bool, issues: [...], normalised_payload: {...}}`.
You do NOT call external APIs. You do NOT log PAN or Aadhaar. When mentioning either in output, mask:
- PAN: `ABCDE1234F` → `ABC***234F`
- Aadhaar: `1234 5678 9012` → `XXXX-XXXX-9012`
''')
print('agent written')
6. Write a hook (hooks/hooks.json + hooks/pan_mask.py)¶
Hooks fire at lifecycle events. We'll write a Python-handler hook (safe-by-default) that masks PAN numbers in every PostTool result. Shell-command hooks are also supported behind plugin_shell_hooks_enabled.
(root / 'hooks' / 'pan_mask.py').write_text('''"""PAN-masking PostTool hook.
Scans tool output for PAN-shaped strings and masks them before the LLM sees them.
"""
import re
from ml_team.core.hooks import HookResult
_PAN_RE = re.compile(r"\\b([A-Z]{3})([A-Z]{2})(\\d{4})([A-Z])\\b")
def handle(ctx: dict) -> HookResult:
content = ctx.get("result", {}).get("content", "")
if not isinstance(content, str):
return HookResult(carry_on=True, mutation=None)
masked = _PAN_RE.sub(lambda m: f"{m.group(1)}***{m.group(3)}{m.group(4)}", content)
if masked == content:
return HookResult(carry_on=True, mutation=None)
return HookResult(carry_on=True, mutation={"result": {"content": masked}})
''')
(root / 'hooks' / 'hooks.json').write_text(json.dumps({
'hooks': {
'PostTool': [{
'handler': {
'type': 'python',
'module': 'pan_mask:handle',
'priority': 100,
},
}],
},
}, indent=2))
print('hook written')
7. Write a README (optional but good practice)¶
(root / 'README.md').write_text('''# bfsi-helper
BFSI helpers for swarm / Claude Code plugins:
- **Skill** — BFSI acronym + regulator glossary (PAN, Aadhaar, CIBIL, RBI, DPDPA, …)
- **Command** — `/explain-pan <pan>` for format validation
- **Agent** — `kyc-validator` for KYC payload validation
- **Hook** — auto-masks PAN numbers in all tool outputs
## Install
```bash
# In swarm
swarm plugins install /path/to/bfsi-helper
# In Claude Code
/plugin install /path/to/bfsi-helper
```
## License
Apache-2.0.
''')
print('README written')
8. Install into swarm¶
import subprocess
result = subprocess.run(['swarm', 'plugins', 'install', str(root)], capture_output=True, text=True)
print(result.stdout)
print(result.stderr)
9. Verify every surface registered¶
import os, httpx
SWARM_API = os.environ.get('SWARM_API', 'http://localhost:8000')
SWARM_TOKEN = os.environ['SWARM_TOKEN']
# List installed plugins
plugins = httpx.get(f'{SWARM_API}/api/v1/plugins',
headers={'Authorization': f'Bearer {SWARM_TOKEN}'}).json()
for p in plugins['plugins']:
if p['name'] == 'bfsi-helper':
print(p)
# Skills
skills = httpx.get(f'{SWARM_API}/api/v1/skills?plugin=bfsi-helper',
headers={'Authorization': f'Bearer {SWARM_TOKEN}'}).json()
print(f'\nSkills: {len(skills["skills"])}')
# Commands
cmds = httpx.get(f'{SWARM_API}/api/v1/plugins/commands',
headers={'Authorization': f'Bearer {SWARM_TOKEN}'}).json()
print(f'Commands: {sum(1 for c in cmds["commands"] if c["plugin"] == "bfsi-helper")}')
# Agents
agents = httpx.get(f'{SWARM_API}/api/v1/plugins/agents?plugin=bfsi-helper',
headers={'Authorization': f'Bearer {SWARM_TOKEN}'}).json()
print(f'Agents: {len(agents["agents"])}')
# Hooks
hooks = httpx.get(f'{SWARM_API}/api/v1/hooks?plugin=bfsi-helper',
headers={'Authorization': f'Bearer {SWARM_TOKEN}'}).json()
print(f'Hooks: {hooks["total"]}')
Expected:
Skills: 1
Commands: 1
Agents: 1
Hooks: 1
10. Try the command + agent¶
# Invoke the /explain-pan command with a sample PAN
resp = httpx.post(
f'{SWARM_API}/api/v1/plugins/commands/bfsi-helper/explain-pan/invoke',
headers={'Authorization': f'Bearer {SWARM_TOKEN}'},
json={'arguments': 'ABCDE1234F'},
)
print(resp.json()['rendered'])
11. Uninstall (cleanup)¶
subprocess.run(['swarm', 'plugins', 'uninstall', 'bfsi-helper'])
Summary¶
In ~20 minutes you built a plugin that ships:
- Procedural knowledge (skill)
- An explicit-invocation command
- A specialized sub-agent
- A Python hook for PII masking
The same directory works in Claude Code (tested against marketplace plugins) and swarm (via our CC-compatible loader). Ship it via git push — no package registry required.
Next¶
- Install a Claude Code plugin — the compat matrix
- Write a custom tool — when you need tools vs skills
- Source code protection — for commercial plugins