GitHub / Plugin Design
Designing for Other People’s Code: A Plugin API That Welcomes Contributions
Make extension safe and approachable: narrow interfaces, documentation-by-example, and sandboxed execution for contributed code.
The best way to attract contributions is to make the path small, clear, and safe. Big, powerful extension APIs are intimidating and fragile. A plugin interface is easier to adopt when it has a small typed contract, a complete example, and clear isolation boundaries for contributed code.
The failure mode
You want contributions, so you design a rich, flexible plugin API. The first contributor struggles to understand how the parts fit together. Another contributor introduces side effects that break core behavior. You spend time triaging issues rather than merging useful additions. The fix is to narrow the contract and improve examples.
What I learned
Keep the interface tiny
Use a single method with a small, typed result. Avoid optional hooks and deep callback trees. If you need flexibility, let plugins implement it internally. Keep core expectations documented and enforced with tests.
Illustrative plugin contract
from dataclasses import dataclass
from typing import Literal, Protocol
@dataclass(frozen=True)
class EvidenceResult:
status: Literal["present", "missing", "unverifiable"]
note: str = ""
class EvidenceSource(Protocol):
name: str
def evaluate(self, profile: dict[str, object]) -> EvidenceResult: ...
Documentation-by-example beats long narratives
Write a short “Adding a plugin” doc that starts with a complete code example and a test. Follow with two variations that show common patterns (e.g., reading a field, combining small signals). Keep the words sparse and the code complete.
Sandboxing contributed code — practical guardrails
Run plugins within constraints:
Timeout per plugin evaluation.
Limit memory-intensive operations or external calls in the demo environment.
Validate types and restrict dangerous imports in plugin modules if possible.
A thread timeout is not a security sandbox: a host process cannot safely terminate arbitrary plugin code. For untrusted extensions, use a separate process or container with resource limits and restricted credentials, or load only trusted plugins.
Versioning and compatibility checks (keep them light)
Require plugins to declare supported interface versions and run contract tests. Keep the declaration simple, and make the CI failure message actionable (e.g., “Plugin supports 1.1.0; core is 1.2.0 — update SUPPORTED_VERSIONS”).
Golden paths and anti-patterns
Show five “golden path” examples contributors should follow:
Pure function: read inputs, return EvidenceResult, no side effects.
Lightweight computation: avoid heavy loops or large allocations.
Deterministic outputs: no random behavior or time-dependent outputs.
Typed result: always return one of the three states with a note.
No external calls: for demo simplicity, avoid network calls.
If you allow external calls, define strict limits and document them clearly.
Error messages designed for contributors
When a plugin fails a contract test, the message should name the plugin, the failing expectation, and the fix. Prefer helpful failures over terse stack traces. Provide a “common issues” page with targeted guidance.
“Hello world” PR built into the docs
Include a numbered guide that results in a merged PR:
Create plugin file.
Add minimal test.
Run pytest -q.
Open PR with the template.
Expected CI jobs and how to interpret them.
Merge expectations.
Trade-offs and failure modes
Trade-offs
Small interface vs expressiveness: Narrow contracts limit advanced features; they also reduce fragility and cognitive load. For small projects, this trade-off is worth it.
Sandboxing vs performance: Timeouts and thread isolation add overhead. Keep limits modest and configurable.
Documentation-by-example vs thorough reference: Examples speed onboarding; deep reference can come later if demand arises.
Failure modes
“Stealth side effects”: Plugins write to global state or files unexpectedly. Solution: sandbox and contract tests; make side-effect-free behavior a rule.
“Doc drift”: Examples fall behind reality. Solution: run example snippets in CI and fail when they don’t compile.
“Silent timeouts”: Sandboxing hides errors without surfacing enough context. Solution: attach structured error notes to EvidenceResult.
Try it yourself
Shrink your plugin interface to a single method that returns a small typed object.
Add a sandbox wrapper with timeouts and typed error handling. Run plugins through it in CI contract tests.
Write a “hello world plugin” guide with a copy-paste example and a minimal test, then merge a sample PR to validate the path.