Create a module
How to build a DevBot module and hook it up as an external module.
Create a module
A module is a self-contained capability unit — skills, tools, hooks, commands, agents, or MCP servers — that DevBot wires into every project. You can build a module inside dev-bot (at src/agentic/<name>/) or in your own repository and register it as an external module.
Module anatomy
Every module follows the same structure; all entries are optional — include only what your module needs. See Module Reference for the full anatomy.
<module>/
agents/ Agent profiles
commands/ Repeatable instruction sets invocable via agent input
skills/ Agent-readable skill instructions (SKILL.md per skill)
hooks/ Event-driven hooks
git/ Git hooks (optional)
hooks.json Declarative hook manifest (harness-agnostic)
tools/ Executable tools
opencode/ TS thin wrapper for the OpenCode tool palette
claudecode/ MCP server script
memory/ Bootstrap files wired into `.agents/memory/` (external modules)
tests/ BATS test suite
install.sh Idempotent OS dependency installer
update.sh Dependency update script
init.sh Per-project init + dependency self-heal
up.sh Post-docker startup script
pre.sh Prerequisites check
functions.sh Thin wrapper sourcing `src/_shared/functions.sh`
mcp.json Canonical MCP server manifest (harness-agnostic — see [MCP configuration](/mcp-config))
plugin.opencode.json OpenCode plugin names declared by this module (optional)
external-modules.json External module dependencies declared by this module
1. Scaffold the module
Create the directory and a functions.sh that sources the shared library:
# src/agentic/<name>/functions.sh
#!/usr/bin/env bash
set -euo pipefail
MODULE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export MODULE_DIR
source "$MODULE_DIR/../../_shared/functions.sh"
This gives your lifecycle scripts access to the shared helpers (_info, _ok, _warn, _error, _step, …).
2. Add capabilities
Skills
Skills are SKILL.md files under skills/<skill-name>/, each with YAML frontmatter (name and description are required). Load the devbot:create-skill skill when authoring one.
skills/<skill-name>/SKILL.md
Tools
Tools are .ts scripts (source of truth) with a thin .sh CLI wrapper. Place each tool in its own directory:
tools/<tool-name>/
<tool-name>.ts Source of truth — business logic + OpenCode tool export
<tool-name>.sh Thin CLI wrapper — delegates to the .ts via bun
The .sh wrapper must handle a mcp-meta subcommand that prints tool metadata as JSON — otherwise the tool is not exposed via the devbot-tools MCP server:
case "${1:-}" in
mcp-meta)
cat <<'JSON'
{"name":"my-tool","description":"What it does","parameters":{"type":"object","properties":{"args":{"type":"array","items":{"type":"string"},"description":"CLI args"}},"required":["args"]}}
JSON
exit 0 ;;
esac
Resolve your own directory symlink-safely (readlink -f) since tools are invoked through symlinks.
Hooks
Declare hooks in hooks.json (harness-agnostic) — the business logic lives in tools/, and each harness wires the manifest through a single generic adapter (on-hooks.ts for OpenCode, on-hooks.py for Claude Code). See Hooks for the schema and the six semantic events. Harness-specific side effects (e.g. prompt injection, two-phase trigger flows) stay hand-written in src/harnesses/<harness>/hooks/.
Commands
Repeatable instruction sets under commands/<command-name>.md, with name/description frontmatter.
Agents
Agent profiles under agents/<name>.md, with name, description, and mode (primary or subagent) frontmatter.
MCP servers
Declare the MCP server(s) once in a canonical, harness-agnostic mcp.json — both harnesses register from it via the shared translator during devbot init (see MCP configuration for the schema, tokens, and per-harness shapes):
{
"mcp": {
"my-mcp": {
"type": "stdio",
"command": ["bash", "-c", "exec {harness-dir}/my-mcp-serve.sh"]
}
}
}
Enabling/disabling the module is the main gate. A server can also declare "enabled": false to ship wired but not started (the manifest entry below would become "enabled": false, right after type) — opencode honors it; claudecode drops the key, since .mcp.json has no per-server on/off. If the module’s integration with a harness is plugin-based (like codebase-index on opencode), declare it in plugin.opencode.json instead — the opencode registration adapter skips plugin-provided servers to avoid double-loading.
Documentation (docs.md)
A module ships its own documentation in a docs.md at the module root. The docs site build gathers it and publishes one page per module at /modules/<area>/<name>; a module without a docs.md gets no page and no link anywhere on the site.
Front matter declares the page and a concise manifest of what the module provides; the body is user-facing prose:
---
title: My Module
description: One line — shown on the modules index and in the navigation.
skills: [my-skill]
mcps:
my-mcp: What the server provides
---
What the module does, in a sentence or two.
## What it does
…
## Configuration
No project configuration is required.
descriptionis required — the build fails without it.agents,commands,skills,hooks,plugins,toolsandmcpsdeclare the module’s capabilities. Each takes a plain list of names when the purpose can be read from the file itself (SKILL.md, an agent/command file,hooks.json, a tool’s# description:header), and aname: purposemap where it cannot — typicallymcps.- The front matter subset is deliberately small: scalars,
[a, b]flow lists, and one level ofname: purpose. A YAML block scalar (description: >-) or block sequence (- item) fails the build with an error rather than being silently mis-read. - The build generates a Contents table immediately before
## Configuration, plus a capability-summary strip above the prose, from that manifest — so the manifest is what the reader sees.
make docs-gather builds the pages locally; make docs gathers and serves the site.
3. Lifecycle scripts
Dev-bot runs these automatically, identically for internal and external modules:
| Script | When it runs | Purpose |
|---|---|---|
pre.sh |
devbot install / devbot update |
Check prerequisites (non-destructive) |
install.sh |
devbot install |
Install OS-level dependencies (idempotent) |
update.sh |
devbot update |
Update dependencies |
up.sh |
devbot up |
Post-docker startup (pull models, seed data) |
down.sh |
devbot down |
Pre-teardown cleanup |
init.sh |
devbot init |
Per-project init + dependency self-heal |
reset.sh |
devbot reinit |
Reset per-project state |
All must be idempotent and source functions.sh.
4. Test
Tests use the bats framework (fixtures in tests/fixtures/). make test auto-installs bats if missing.
5. Ship it as an external module
An external module is a standalone repo that only needs the artifact directories — DevBot clones it into vendor/ and symlinks the artifacts into every project. Its lifecycle scripts (pre.sh, install.sh, update.sh, up.sh, down.sh, init.sh, reset.sh) are discovered at the repo root and run by DevBot exactly like internal modules, via storage/external-agentic-modules/<name>/.
Layout in your own repo
Put the artifact directories at the repo root:
your-module/
skills/ # each skill in its own subdirectory
agents/ # agent .md files
commands/ # command .md files
plugins/ # hook/plugin files (optional)
memory/ # bootstrap files wired into .agents/memory/ (optional)
install.sh # optional lifecycle scripts: pre/install/update/up/down/init/reset.sh
Register it
From a git URL (clones into vendor/, auto-detects ./skills, ./agents, ./commands, ./plugins):
devbot module add https://github.com/you/your-module.git
devbot module add https://github.com/you/your-module.git --skills=./my-skills --agents=./my-agents
From a local path (no cloning — symlinked into vendor/; handy for local development):
devbot module add /path/to/your-module
Declared by another module — a module can ship an external-modules.json declaring its own dependencies, which dev-bot auto-merges into .devbot.global.jsonc during devbot install/devbot update:
{
"your-module": {
"url": "https://github.com/you/your-module.git",
"paths": {
"skills": "skills",
"agents": "agents",
},
},
}
After registering, wire it into every project with:
devbot module sync # or: devbot init <path>
Config format
Registration writes an entry under the external_modules key of .devbot.global.jsonc:
"external_modules": {
"your-module": {
"url": "https://github.com/you/your-module.git",
"paths": {
"skills": "skills", // directory symlink — all files linked
"memory": {
"bootstrap.md": "active/bootstrap.md" // file-level symlink
}
}
}
}
paths semantics:
- String value — the whole directory is symlinked into
.opencode/<type>/<name>/. - Object value — each file is symlinked individually at its exact destination (used for
memory/bootstrap files). - Omitted key — that artifact type is not wired.
See also
- Module Reference — full anatomy and the
devbot moduleCLI - Skills / Hooks / MCPs — the shipped artifacts these follow