Plugins & Extensibility
Add new commands, providers, hooks, validators, and formatters without modifying hence
Overview
Hence is extensible via a plugin system modelled on git and cargo: place an executable
named hence-{name} anywhere in your PATH and it becomes available
as hence {name}. No compilation, no manifests, no registration — just an executable.
There are five extension points:
| Extension point | How it works | Use case |
|---|---|---|
| External commands | hence-{name} in PATH |
New subcommands, custom dashboards, CI integrations |
| Custom providers | ~/.config/hence/config.toml |
Register internal LLM endpoints for hence agent spawn/watch |
| Lifecycle hooks | Scripts in ~/.config/hence/hooks/ or .hence/hooks/ |
Slack notifications, metrics, CI triggers on task state changes |
| Validator plugins | Scripts in ~/.config/hence/validators/ or .hence/validators/ |
Custom plan validation rules, team conventions |
| Board formatters | hence-board-{format} in PATH |
HTML dashboards, Slack messages, custom kanban views |
All plugins run in isolated child processes. A crashing or hanging plugin cannot corrupt hence's internal state or plan files.
External commands
Discovery
Hence searches your PATH for executables matching the pattern
hence-{name} at command dispatch time. Any match becomes available as
hence {name}. Built-in commands always take precedence — you cannot shadow
hence board or hence task.
# Create a custom command (any language, any script)
cat > ~/bin/hence-todo <<'EOF'
#!/usr/bin/env bash
# hence-todo: list TODOs in a plan
hence plan board "$1" --json | jq '[.backlog[]]'
EOF
chmod +x ~/bin/hence-todo
# Use it immediately — no registration needed
hence todo plan.spl
Execution
When you run hence {name} [args...], hence invokes hence-{name} [args...]
with the full argument list passed through verbatim. Stdin, stdout, and stderr are connected
to your terminal. The plugin's exit code becomes hence's exit code.
Hence sets the following environment variables for every external command invocation:
| Variable | Value |
|---|---|
HENCE_AGENT | Current agent name (if set) |
HENCE_PLAN | Path to the plan file (if applicable) |
PATH, HOME, etc. | Standard environment, inherited unchanged |
Help integration
hence help. Running
hence todo --help invokes hence-todo --help correctly, but
the "External Commands" section in the main help output is not yet present.
Once implemented, running hence {name} --help will delegate directly to
hence-{name} --help, so your plugin controls its own help text.
Error handling
- Command not found — hence prints "unknown command: NAME. Did you mean …?"
- Not executable — hence prints a permission error with a fix suggestion (
chmod +x) - Non-zero exit — hence propagates the exit code unchanged
Configuration file
Hence loads configuration from two locations, merging them with local values overriding global:
~/.config/hence/config.toml— global, applies to all plans ($XDG_CONFIG_HOME/hence/config.tomlifXDG_CONFIG_HOMEis set).hence/config.toml— plan-local, checked relative to the working directory
Both files use the same TOML schema. The file is optional — hence runs without it.
# ~/.config/hence/config.toml
[settings]
hook_timeout = 30 # seconds before killing a hook (default: 30)
plugin_path = [] # extra directories to search for plugins
# Custom agent providers
[[providers]]
id = "myagent" # used in (meta task-X (provider "myagent"))
name = "My Custom Agent" # display name
cli = "myagent" # executable name or absolute path
version_args = ["--version"] # optional: check version
auto_approve_flag = "-y" # optional: non-interactive mode
initial_prompt_flag = "-p" # optional: flag for initial prompt
use_keystroke_injection = false # optional: default false
keystroke_delay_ms = 100 # optional: default 100
# Hook configuration
[hooks]
post_complete = true # true = use default name; false = disable this hook
post_claim = "notify.sh" # override the hook script name
id, the local definition wins. Scalar settings (like hook_timeout)
are overridden by the local value.
Configuration is validated on load. Syntax errors are reported with file path and line:column. Missing required fields on provider definitions cause an error. Unknown fields produce a warning but do not prevent execution.
Custom agent providers
By default, hence agent spawn and hence agent watch auto-detect
available LLM CLI tools (claude, aider, etc.). You can register additional providers —
for example, an internal endpoint or a wrapper script — via the config file.
[[providers]]
id = "internal-gpt"
name = "Internal GPT"
cli = "/opt/internal/llm-cli"
initial_prompt_flag = "--prompt"
auto_approve_flag = "--yes"
Once registered, select the provider per-task in your SPL plan:
(meta task-deploy
(description "Deploy to staging")
(acceptance "Deployment passes smoke tests")
(provider "internal-gpt"))
Provider selection order is: task metadata → agent config → auto-detection fallback. If the provider CLI is not found at startup, hence prints "provider 'internal-gpt' CLI '/opt/internal/llm-cli' not found" and exits 1.
| Field | Required | Description |
|---|---|---|
id | Yes | Unique identifier, referenced in (meta task (provider "...")) |
name | Yes | Human-readable display name |
cli | Yes | Executable name (searched in PATH) or absolute path |
version_args | No | Args to pass for version check, e.g. ["--version"] |
auto_approve_flag | No | Flag for non-interactive / auto-approve mode |
initial_prompt_flag | No | Flag for passing the initial task prompt |
use_keystroke_injection | No | Use PTY keystroke injection instead of flags (default: false) |
keystroke_delay_ms | No | Delay between injected keystrokes in ms (default: 100) |
resume_flag | No | Flag for resuming an interrupted session |
default_args | No | Array of extra arguments always passed to the provider CLI |
Lifecycle hooks
Hooks are executable scripts that run automatically after specific task state changes. They are post-commit notifications: the state change happens first, unconditionally. A hook failure does not roll back the operation.
Hook points
| Hook name | Fires after |
|---|---|
post-claim | hence task claim |
post-complete | hence task complete |
post-block | hence task block |
post-unblock | hence task unblock |
post-assert | hence task assert |
post-plan-complete | All tasks in the plan reach completed state |
Invocation
Place an executable script at .hence/hooks/{hook-name} (plan-local) or
~/.config/hence/hooks/{hook-name} (global). If both exist, both run
— local first, then global.
# .hence/hooks/post-complete
#!/usr/bin/env bash
# $1 = plan file path, $2 = task name, $3 = agent name
echo "Task $2 completed by $3 in plan $1"
curl -s -X POST "$SLACK_WEBHOOK" \
-d "{\"text\":\"✅ Task \`$2\` completed by \`$3\`\"}"
For post-assert, the second argument is the SPL expression that was asserted
(not a task name). All hooks also receive these environment variables:
| Variable | Value |
|---|---|
HENCE_HOOK | Name of the hook being executed |
HENCE_PLAN | Absolute path to the plan file |
HENCE_TASK | Task name (empty for post-assert) |
HENCE_AGENT | Agent name |
HENCE_EVENT | Event type: claim, complete, block, unblock, assert, plan-complete |
Timeout and failure
Hooks time out after 30 seconds by default (configurable via hook_timeout in
[settings]). On timeout, hence sends SIGTERM then SIGKILL after 5 seconds.
Hook failure modes and their handling:
| Condition | Behaviour |
|---|---|
| Hook file not found | Silently skipped (not an error) |
| Hook not executable | Warning: "hook 'NAME' is not executable, skipping" |
| Hook exits non-zero | Warning: "hook 'NAME' failed (exit N)" |
| Hook times out | Warning: "hook 'NAME' timed out after Ns" |
mkdir -p .hence/hooks
cat > .hence/hooks/post-complete <<'EOF'
#!/usr/bin/env bash
echo "[$HENCE_EVENT] task=$HENCE_TASK agent=$HENCE_AGENT"
EOF
chmod +x .hence/hooks/post-complete
Validator plugins
Custom validators extend hence plan validate with team-specific rules.
They are scripts that receive the plan file path and output a JSON array of diagnostics.
Discovery and execution
Place executable scripts in .hence/validators/ (plan-local) or
~/.config/hence/validators/ (global). All scripts in both
directories are executed — unlike hooks, there is no name-based matching. Local
validators run first.
#!/usr/bin/env bash
# .hence/validators/require-descriptions
# Checks that all tasks have (description "...") in their meta.
PLAN="$1"
# Use hence itself to get board JSON, then check descriptions
hence plan board "$PLAN" --json | python3 -c "
import json, sys
tasks = json.load(sys.stdin)
issues = []
for status, items in tasks.items():
for t in items:
if not t.get('meta', {}).get('description'):
issues.append({'level': 'warning', 'message': f'task {t[\"name\"]} has no description'})
print(json.dumps(issues))
"
Output schema
The validator must write a JSON array to stdout. Each element is a diagnostic object:
[
{
"level": "warning",
"message": "task deploy has no description",
"line": 42,
"rule": "require-descriptions"
}
]
| Field | Required | Description |
|---|---|---|
level | Yes | "warning" or "error" |
message | Yes | Human-readable description |
line | No | Line number in the plan file |
rule | No | Rule or label reference |
Diagnostics from each validator are merged with built-in results and prefixed with
[validator-name] in the output. With --strict, custom warnings
are treated as errors (exit 1).
Validators also receive HENCE_PLAN (absolute path) and HENCE_STRICT
("true" or "false") as environment variables.
hence plan validate plan.spl # run built-in + custom validators
hence plan validate plan.spl --strict # warnings become errors
Failure modes
- Not found — no custom validation runs (not an error)
- Not executable — warning printed, validator skipped
- Malformed JSON — warning: "validator 'NAME' output invalid JSON, skipping"
- Timeout — warning: "validator 'NAME' timed out"
- Non-zero exit with no output — warning: "validator 'NAME' failed"
Board formatter plugins
hence plan board supports custom output formats via the --format flag.
Built-in formats are kanban, tree, dag, and json.
Additional formats are discovered as hence-board-{format} executables in PATH.
# Use a custom HTML formatter
hence plan board plan.spl --format html
# Internally, hence does:
hence plan board plan.spl --json | hence-board-html
The formatter receives the full board JSON on stdin and writes its output to stdout.
The JSON schema is the same as hence plan board --json:
{
"plan": "path/to/plan.spl",
"tasks": [
{
"name": "auth",
"state": "ready",
"assigned_to": ["coder"],
"meta": { "description": "...", "acceptance": "..." }
}
],
"conclusions": [ "...", "..." ],
"meta": { "title": "My Plan", "status": "active" }
}
tasks, meta,
and conclusions fields being present.
A non-zero exit from the formatter causes hence to print
"formatter 'html' failed (exit N)". Built-in formats always take precedence —
a hence-board-json in PATH will never override the built-in --format json.
Example: HTML formatter
#!/usr/bin/env python3
# hence-board-html — render board as a simple HTML page
import json, sys
board = json.load(sys.stdin)
title = board["meta"].get("title", "hence board")
print(f"<!DOCTYPE html><html><head><title>{title}</title></head><body>")
print(f"<h1>{title}</h1><ul>")
for task in board["tasks"]:
state = task["state"]
print(f'<li><b>{task["name"]}</b> — {state}</li>')
print("</ul></body></html>")
Worktree file provisioning
When hence agent spawn creates an isolated git worktree for an agent, you may
need certain files (like .env or shared config) to be available in the worktree.
The [worktree.provision] section in config.toml controls which files
are symlinked or copied into agent worktrees.
# In ~/.hence/config.toml or .hence/config.toml
[worktree.provision]
files = [
{ from = ".env", to = ".env", mode = "copy" },
{ from = "config/shared.toml", to = "config/shared.toml" }, # default: symlink
]
Files with mode = "copy" are duplicated into the worktree (useful for secrets
that should not be symlinked). The default mode is symlink.
Quick reference
| What you want | How to do it |
|---|---|
New subcommand hence foo |
Create hence-foo executable in PATH |
Custom board format --format csv |
Create hence-board-csv executable in PATH; reads board JSON from stdin |
| Run code on task completion | Create .hence/hooks/post-complete (executable) |
| Run code after entire plan finishes | Create .hence/hooks/post-plan-complete (executable) |
| Custom validation rules | Create .hence/validators/my-rule (executable, outputs JSON diagnostics) |
| Register a custom LLM CLI | Add [[providers]] entry to ~/.config/hence/config.toml |
| Change hook timeout | Set [settings] hook_timeout = N in config.toml |
| Add extra plugin search path | Set [settings] plugin_path = ["/opt/hence-plugins"] in config.toml |