Configuration
Extensions
This page shows how to extend agents with skills, project instructions, MCP servers, hooks and plugins.
| You want to… | Use |
|---|---|
| Give the agent reusable know-how it loads only when relevant | Skills |
| Tell the agent how a project works | Project instructions |
| Add tools from an external server | MCP servers |
| Run your own scripts at fixed points, for example to block a command or format code | Hooks |
| Install a bundle of skills, hooks and MCP servers in one step | Plugins |
After changing any of these files, run openvaala config check and openvaala serve reload. See Apply changes.
Skills
A skill is a directory with a SKILL.md file. The agent sees each skill's name and description, and reads the full file when a task calls for it.
---
name: release-notes
description: Use when drafting release notes from merged pull requests.
---
# Release notes
1. List merged pull requests since the last tag ...- The file must start with YAML frontmatter.
nameuses lowercase letters, digits and single hyphens (release-notes, notRelease_Notes). It defaults to the directory name.descriptionis required. Write it as when to use the skill; that is what the agent decides on.- Other frontmatter fields are ignored. Files above 1 MB are not read.
Where skills are found
OpenVaala looks in these places, in this order. When two skills share a name, the first one wins.
.agents/skills/in the conversation's working directory.claude/skills/in the conversation's working directory- Your private skills directory,
~/.config/openvaala/skills/(skills.directory) - Built-in skills on the
openvaalaexecutor, under/skills - Skills from enabled plugins, named
<plugin>:<skill> ~/.agents/skills/in your home directory~/.claude/skills/in your home directory
The working directory and home directory are those of the conversation's default executor, so a conversation on an SSH executor picks up skills from the remote machine.
Directories are searched recursively. Once a directory contains SKILL.md, its subdirectories are not searched further. A symbolic link can point straight at a single skill directory. A malformed skill in your private skills directory or the built-in set is an error; elsewhere it is skipped.
Using a skill directly
Type / in the terminal UI, web UI, desktop app or your editor to see the available skills, then pick one, for example /release-notes. The skill is attached to your message so the agent follows it from the start. Plugin skills appear as /<plugin>:<skill>. You can also write /skill:<name>: unlike a bare /<name>, which stays plain text when no such skill exists, it is rejected if the skill is not available to the agent.
The built-in OpenVaala skill
OpenVaala ships one built-in skill, openvaala: this documentation. Ask an agent how to configure OpenVaala and it can read the relevant page and edit your configuration for you.
Skills are discovered when a conversation opens or is reconfigured. After adding a skill, reload the configuration or start a new conversation.
Project instructions
Put project conventions — how to build, how to run tests, what not to touch — in AGENTS.md files. OpenVaala reads them for you; there is nothing to configure.
- At the start of every turn, the agent receives each
AGENTS.mdfrom the root of the Git repository that contains the working directory down to the working directory itself, on the default executor. Outside a Git repository, only the working directory'sAGENTS.mdis read. - When the agent reads a file deeper in the tree,
AGENTS.mdfiles between the working directory and that file are added too. On other executors, theirAGENTS.mdfiles are added after the agent first uses a tool there. - Deeper files take precedence over higher ones, and what you ask for directly takes precedence over all of them.
- Each file enters the context once, and comes back after the context is compacted.
Only files named AGENTS.md are read; CLAUDE.md and files in your home directory are not. Use an agent's instructions for guidance that applies everywhere.
MCP servers
Each file in ~/.config/openvaala/mcp_servers/ declares one Model Context Protocol server. Its tools appear to the model as mcp__<id>__<tool>.
# mcp_servers/serena.yaml — a local process
id: serena
type: stdio
command: uvx
args: [serena, start-mcp-server]
executors: [local, devbox] # run it on these executors
required: true # fail the turn if it cannot connect# mcp_servers/context7.yaml — a remote server
id: context7
type: http
url: https://mcp.context7.com/mcp
headers:
CONTEXT7_API_KEY: ...Fields:
id— required, matching the file name. Letters, digits,-and_only, and no__.type—stdio,httporsse.command,args,env— forstdio.commandis required.envis added on top of the inherited environment.urlandheadersare not allowed.url,headers— forhttpandsse.urlmust be anhttp(s)URL.command,argsandenvare not allowed.executors— the executors the server runs on; defaults to[local]. Each id must belocalor an executor fromexecutors/that is notvirtual. The built-inopenvaalaexecutor cannot run MCP servers.tools— the tool names (as the server calls them) to offer the model. Defaults to all.required— whentrue, a connection failure fails the turn. By default the server's tools are skipped instead.disabled—truekeeps the file but does not use the server.startup_timeout_seconds— how long one connection attempt may take; default 30.tool_timeout_seconds— how long one tool call may take; default 300.
Values are used as written; ${VAR} is not expanded.
Where it runs. A stdio server is started in the executor's working directory, on the executor's machine. http and sse servers are contacted from the executor's machine. Listing an SSH executor therefore runs the server on that remote host.
Which agents get it. An agent's mcp_servers list chooses servers by id; leaving it out gives the agent every server, and [] gives it none. See Agents.
Servers from your editor. When you connect through ACP, the MCP servers configured in your editor (stdio, http or sse) are also available in that conversation. They run on the editor's machine and their messages are relayed through the connection, so they disappear when the editor disconnects. See Editors.
Check connection status with /mcp in the terminal UI or the MCP tab of the conversation settings in the web UI.
Hooks
Hooks run your commands at fixed points in a conversation. A hook can block a tool call, rewrite its arguments, add context for the model, or stop a turn from ending.
Put any number of *.yaml files in ~/.config/openvaala/hooks/; all of them apply. Other files there, such as scripts, are not read as configuration.
# hooks/go.yaml
hooks:
PreToolUse:
- matcher: bash
hooks:
- type: command
script: guard.sh # relative to this yaml file
args: [--strict]
timeout: 10
on_failure: block
PostToolUse:
- matcher: apply_patch
hooks:
- type: command
script: gofmt.sh
Stop:
- hooks:
- type: command
command: ~/bin/check-tests.sh # run as-is on the target machine
timeout: 600Events
| Event | When it runs | matcher is compared with |
|---|---|---|
SessionStart | A conversation starts or is resumed | source: startup or resume |
UserPromptSubmit | You send a message | — (always matches) |
PreToolUse | Before a tool call | tool_name |
PostToolUse | After a tool call | tool_name |
Stop | The agent is about to finish its turn | — (always matches) |
SubagentStart | A subagent starts | agent_type |
SubagentStop | A subagent is about to finish | agent_type |
Hooks run on the executor where the event happens, with the executor's working directory as the current directory.
Matchers
- Empty, missing or
*matches everything. - A pattern made only of letters, digits,
_,-and|is a list of exact names:bash|apply_patch. - Anything else is a Go regular expression, not anchored.
For tool events a few aliases are understood, so hooks written for other agents keep working: Bash → bash, Write and Edit → apply_patch, Read → read_file, Agent → spawn_agent.
Hook fields
type— required; onlycommandis supported.command— a command line passed tobash -con the target machine. Programs it uses must exist there.script— a script file on the runtime server's machine: relative to the YAML file, absolute, or starting with~/. It must exist when the configuration loads, start with a shebang and be executable.- Write exactly one of
commandandscript. args— arguments appended toscript, each shell-quoted. Only valid withscript.timeout— seconds; default 60; must be positive.on_failure—allow(default) orblock: what to do when the hook itself fails (times out, exits with a code other than 0 or 2, or prints invalid output). Only valid forPreToolUseandUserPromptSubmit.
Before each run, a script is copied to ~/.cache/openvaala/hooks/<hash>/<file name> on the target machine — local and SSH executors alike — and copied again only when its content or permissions change. Only that one file is copied: interpreters and other files it needs must already exist on the target. Script edits take effect on the next run; YAML edits need openvaala serve reload.
Input and output
The hook receives a JSON object on stdin with hook_event_name, session_id (the conversation id), turn_id and cwd, plus event fields:
SessionStart:sourceUserPromptSubmit:promptPreToolUse:tool_name,tool_input,tool_use_id;PostToolUseaddstool_responseStop,SubagentStop:stop_hook_active,last_assistant_message- In a subagent:
agent_idandagent_type
The hook answers with its exit code and output:
- Exit 2 blocks; stderr is the reason.
SessionStartandSubagentStartcannot block, so exit 2 there counts as a hook failure. - Exit 0, empty stdout lets things proceed. stderr only goes to the log.
- Exit 0, plain text is added as context for the model on
SessionStart,SubagentStartandUserPromptSubmit, and ignored on other events. - Exit 0, stdout starting with
{is parsed as JSON; invalid JSON is a hook failure. Recognized fields:decision: "block"withreasonblocksUserPromptSubmit,PostToolUse,StopandSubagentStop.hookSpecificOutput.permissionDecision: "deny"withpermissionDecisionReasonblocks aPreToolUsecall.allowis accepted;askanddeferare treated as hook failures.hookSpecificOutput.updatedInputreplaces the tool arguments entirely (PreToolUseonly).hookSpecificOutput.additionalContextadds context for the model.continue: falsewithstopReasonends the turn.systemMessageis shown to you, not the model.
- Any other exit code or a timeout is a hook failure.
stdout and stderr are each truncated at 64 KB. For example, a PostToolUse formatter can report what it did:
{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "gofmt reformatted main.go"}}The web UI lists loaded hooks under Settings → Hooks and shows each hook run in the conversation.
Plugins
A plugin is a directory that bundles skills, hooks and MCP servers. Install one from git or a local directory:
openvaala plugin install https://github.com/me/my-plugins.git --path plugins/review --ref v1.2.0
openvaala plugin list
openvaala plugin update # all plugins installed with `plugin install`
openvaala plugin disable review
openvaala plugin enable review
openvaala plugin uninstall review--pathpoints at the plugin inside the repository (default: the repository root);--refpicks a branch or tag (default: the default branch, git sources only). Installing from git needsgit.plugin listprints one line per plugin: name, version,enabledordisabled, and source.- These commands change only the plugins directory and
plugins.disabled, and they do not need the configuration to load beforehand: you can disable, uninstall or update a plugin that keeps the configuration from loading. After the change they load the full configuration and undo the change if it fails to load. On success they reload the local runtime server. New plugin skills appear in conversations you start or reopen afterwards. - You can also copy or symlink a plugin directory into
plugins/yourself, then runopenvaala serve reload.
The manifest
A native plugin describes itself in .openvaala-plugin/plugin.yaml at its root:
name: review # required; must equal the directory name
version: 1.2.0 # optional, shown in plugin list
description: Code review helpers # optional
skills: ./skills/ # optional; this is the default
hooks: # optional; same format as hooks/*.yaml
SessionStart:
- hooks:
- type: command
script: ./hooks/inject
args: [SessionStart]
mcp_servers: # optional; same fields as mcp_servers/<id>.yaml, without executors
- id: review-docs
type: stdio
command: node
args: ["${OPENVAALA_PLUGIN_ROOT}/mcp/server.mjs"]- Name: lowercase letters, digits and single hyphens, equal to the directory name.
skillis reserved. - Skills: found under
skills(relative to the plugin root; missing means none) and named<plugin>:<skill>, such asreview:checklist. Use that full name in an agent'sskillslist and as/review:checklist. A malformed plugin skill is skipped. - Hooks: same events, matchers and fields as
hooks/*.yaml, but ascriptmust be a relative path inside the plugin directory. Hooks fromhooks/run first, then plugin hooks in plugin-name order. - MCP servers: run on
localonly. Ifmcp_servers/has a file with the same id, that file wins and the plugin's entry is skipped — use this to move the server to another executor or add an API key. Two enabled plugins declaring the same id is an error. While an agent'smcp_serverslist names a plugin's server,plugin disableandplugin uninstallrefuse to remove it. ${OPENVAALA_PLUGIN_ROOT}: in a stdio MCP server'scommand,argsandenv, it is replaced with the plugin's absolute directory, and the server also gets it as an environment variable. Hooks receive it as an environment variable, socommand: cat "${OPENVAALA_PLUGIN_ROOT}/core.md"works. Onlocalit points at the plugin directory. On other executors the whole plugin directory is first copied to~/.cache/openvaala/plugins/<name>-<hash>/on that machine (this needstarthere), so scripts can read the plugin's other files.plugin installwrites.openvaala-install.yaml(source, ref, path, commit) into the plugin directory;plugin updateuses it. Do not edit it.
Codex and Claude plugins
Plugins that ship .codex-plugin/plugin.json or .claude-plugin/plugin.json can be installed as they are. OpenVaala reads only their name, version, description and skills — one path or a list of paths, searched after the default ./skills/, each either a directory of skills or a single skill's directory — and does not load their hooks, MCP servers or other components. If a plugin has more than one manifest, the first of OpenVaala, Codex, Claude is used. Source manifests are never modified.
Directories in plugins/ without any manifest, and names starting with ., are skipped. A broken manifest makes the configuration fail to load, so the runtime server will not start, and a reload keeps the old configuration.