Shape CLI is extensible through plugins. This guide covers using and developing plugins.
Plugins communicate with Shape via JSON over stdin/stdout. This means you can write plugins in any language.
Plugin types:
| Type | Purpose | Binary naming |
|---|---|---|
| Brief Type | Custom document templates | shape-brief-<name> |
| Sync | Bidirectional sync with external tools | shape-sync-<name> |
shape advanced plugin listshape advanced plugin show shape-sync-githubShape finds plugins in:
.shape/plugins/in the project directory- Directories in
$PATH
Plugins must:
- Be executable
- Follow naming convention:
shape-{type}-{name} - Support
--manifestflag for capability discovery
Brief type plugins define custom document templates.
minimal— Basic title and statusshapeup— Full ShapeUp pitch template
A brief type plugin must support these operations:
shape-brief-rfc --manifestResponse:
{
"name": "rfc",
"version": "1.0.0",
"description": "RFC (Request for Comments) brief type",
"operations": ["template", "validate"]
}Returns the markdown template for new briefs.
Request (via stdin):
{
"operation": "template",
"params": {
"title": "API Versioning Strategy"
}
}Response:
{
"success": true,
"template": "---\nid: {{id}}\ntitle: {{title}}\nstatus: proposed\ntype: rfc\n---\n\n# {{title}}\n\n## Summary\n\nBrief description of the proposal.\n\n## Motivation\n\nWhy are we doing this?\n\n## Detailed Design\n\nExplain the design in detail.\n\n## Alternatives Considered\n\nWhat other approaches were considered?\n\n## Unresolved Questions\n\nWhat is still TBD?\n"
}Validates brief content against type-specific rules.
Request:
{
"operation": "validate",
"params": {
"content": "---\nid: b-123\ntitle: Test\nstatus: proposed\ntype: rfc\n---\n\n# Test\n\n## Summary\n\nA test RFC.\n"
}
}Response:
{
"success": true,
"valid": true,
"errors": []
}Or with errors:
{
"success": true,
"valid": false,
"errors": [
"Missing required section: Motivation",
"Missing required section: Detailed Design"
]
}#!/usr/bin/env python3
import json
import sys
MANIFEST = {
"name": "rfc",
"version": "1.0.0",
"description": "RFC brief type",
"operations": ["template", "validate"]
}
TEMPLATE = """---
id: {{id}}
title: {{title}}
status: proposed
type: rfc
created: {{created}}
---
# {{title}}
## Summary
Brief description of the proposal.
## Motivation
Why are we doing this?
## Detailed Design
Explain the design in detail.
## Alternatives Considered
What other approaches were considered?
## Unresolved Questions
What is still TBD?
"""
REQUIRED_SECTIONS = ["Summary", "Motivation", "Detailed Design"]
def handle_template(params):
return {"success": True, "template": TEMPLATE}
def handle_validate(params):
content = params.get("content", "")
errors = []
for section in REQUIRED_SECTIONS:
if f"## {section}" not in content:
errors.append(f"Missing required section: {section}")
return {
"success": True,
"valid": len(errors) == 0,
"errors": errors
}
def main():
if "--manifest" in sys.argv:
print(json.dumps(MANIFEST))
return
request = json.loads(sys.stdin.read())
operation = request.get("operation")
params = request.get("params", {})
if operation == "template":
response = handle_template(params)
elif operation == "validate":
response = handle_validate(params)
else:
response = {"success": False, "error": f"Unknown operation: {operation}"}
print(json.dumps(response))
if __name__ == "__main__":
main()Save as .shape/plugins/shape-brief-rfc, make executable, then:
shape brief new "API Versioning" --type rfcSync plugins enable bidirectional sync with external tools.
shape advanced sync run github
shape advanced sync statusA sync plugin must support these operations:
shape-sync-github --manifestResponse:
{
"name": "github",
"version": "1.0.0",
"description": "Sync with GitHub Issues",
"operations": ["push", "pull", "status"],
"config_schema": {
"repo": {"type": "string", "required": true},
"token": {"type": "string", "required": true, "env": "GITHUB_TOKEN"}
}
}Push local changes to external tool.
Request:
{
"operation": "push",
"params": {
"briefs": [...],
"tasks": [...],
"config": {
"repo": "owner/repo",
"token": "ghp_..."
}
}
}Response:
{
"success": true,
"pushed": {
"briefs": 2,
"tasks": 5
},
"mappings": [
{"local": "b-7f2a3b1", "remote": "123"},
{"local": "b-7f2a3b1.1", "remote": "124"}
]
}Pull changes from external tool.
Request:
{
"operation": "pull",
"params": {
"config": {
"repo": "owner/repo",
"token": "ghp_..."
},
"since": "2025-01-15T00:00:00Z"
}
}Response:
{
"success": true,
"briefs": [...],
"tasks": [...],
"mappings": [...]
}Check sync status.
Request:
{
"operation": "status",
"params": {
"config": {...}
}
}Response:
{
"success": true,
"connected": true,
"last_sync": "2025-01-16T10:30:00Z",
"pending_push": 2,
"pending_pull": 0
}Shape stores sync state in .shape/sync/:
.shape/
├── sync/
│ ├── github.jsonl # ID mappings for GitHub
│ └── linear.jsonl # ID mappings for Linear
These files map local IDs to remote IDs and are git-ignored by default.
Configure plugins in .shape/config.toml:
[plugins.sync.github]
repo = "owner/repo"
# token read from GITHUB_TOKEN env var
[plugins.sync.linear]
team = "ENG"
# api_key read from LINEAR_API_KEY env var
[plugins.brief.rfc]
require_motivation = truePlugins should return errors in a consistent format:
{
"success": false,
"error": "Authentication failed: invalid token",
"code": "AUTH_ERROR"
}Error codes help Shape handle errors appropriately:
AUTH_ERROR— Prompt for credentialsRATE_LIMIT— Retry with backoffNOT_FOUND— Resource doesn't existVALIDATION— Invalid input
# Test plugin discovery
shape advanced plugin list
# Test plugin manifest
shape advanced plugin show shape-sync-github
# Test connectivity (for sync plugins)
shape advanced sync status
# Dry run
shape advanced sync run github --dry-runBrief Types:
adr— Architecture Decision Recordsepic— Agile epics with acceptance criteriabug— Bug report templatespike— Research/investigation template
Sync:
linear— Linear.appjira— Atlassian Jiranotion— Notion databasesgithub— GitHub Issuesgitlab— GitLab Issuesasana— Asana tasks