Qwen Code Rust supports plugins that extend the agent with slash commands, skills, MCP servers, agent definitions, and lifecycle hooks. The plugin format is compatible with Claude Code plugins.
Plugins are discovered from two locations:
| Location | Scope | Precedence |
|---|---|---|
~/.qcr/plugins/ |
Global — available in every project | Lower |
.qcr/plugins/ |
Project-local — current project only | Higher |
When a global plugin and a project plugin share the same name, the project plugin takes precedence.
Each plugin is a directory containing a metadata file. All other subdirectories are optional:
my-plugin/
├── .claude-plugin/ ← use this for Claude Code compatibility
│ └── plugin.json
├── commands/ ← slash commands (one .md file per command)
│ ├── deploy.md
│ └── review.md
├── skills/ ← skill instruction files (.md with YAML frontmatter)
│ └── rust-patterns.md
├── agents/ ← sub-agent definitions (.md or .json)
│ └── reviewer.md
├── hooks/
│ └── hooks.json ← lifecycle hook configuration
├── .mcp.json ← additional MCP server configs
└── README.md
Qwen Code Rust also accepts .qcr-plugin/plugin.json for qcr-specific
plugins that are not intended for use with Claude Code.
Every plugin must have a plugin.json inside .claude-plugin/ or
.qcr-plugin/.
{
"name": "feature-dev",
"version": "1.0.0",
"description": "Comprehensive feature development workflow",
"author": {
"name": "Alice",
"email": "alice@example.com"
},
"category": "development"
}| Field | Required | Description |
|---|---|---|
name |
Yes | Unique plugin identifier. Used as namespace prefix for commands and MCP servers. |
version |
No | Semver version string. |
description |
No | Human-readable description. |
author.name |
No | Author display name. |
author.email |
No | Author email address. |
category |
No | Plugin category (e.g. "development", "productivity"). |
Unknown fields are ignored, so the schema is forward-compatible.
Each .md file in commands/ defines one slash command. The filename stem
becomes the command name.
commands/deploy.md → /deploy
commands/review.md → /review
The entire file content is the command prompt. When a user types /deploy,
qcr injects the file content as a user message and starts an agent turn.
Use $ARGUMENTS anywhere in the file to receive the text typed after the
command name:
Deploy the application to the `$ARGUMENTS` environment.
Steps:
1. Run the test suite.
2. Build a release artifact.
3. Upload and restart the service.Invocation: /deploy staging → $ARGUMENTS is replaced with staging.
If $ARGUMENTS is absent from the content and the user typed extra text,
that text is appended to the content on a new line.
The first non-empty, non-heading line in the file is used as the one-line description shown in help output.
Commands are resolved by short name first. If two plugins define /deploy,
typing /deploy shows an ambiguity error listing both
plugin-a:deploy and plugin-b:deploy. Use the namespaced form
/plugin-a:deploy to be unambiguous.
Each .md file in skills/ defines a skill. Qwen Code Rust parses YAML frontmatter
for metadata and uses the body as the instruction content.
---
name: rust-patterns
description: Idiomatic Rust coding patterns
tags: [rust, patterns]
---
Prefer iterators over explicit loops. Use `?` for error propagation.
Avoid `unwrap()` in library code; prefer `expect()` with descriptive messages.Skills contributed by plugins are registered into the global SkillManager
and are available to the GetSkill tool during an agent session.
When multiple skills share the same name, the first match in this order wins:
- Project plugin skills —
.qcr/plugins/<plugin>/skills/<name>.md - Global plugin skills —
~/.qcr/plugins/<plugin>/skills/<name>.md - Project skills —
.qcr/skills/<name>.md - Built-in skills — compiled into the binary; always available, never overwrite user skills
The following skills ship with qcr and require no installation:
| Name | Tags | Description |
|---|---|---|
spec-driven-development |
planning, spec, workflow |
Structured Requirements → Design → Tasks pipeline. Five phases: Requirements → Design → Tasks → Execution → E2E Verification. Includes auto-continue rules, review checkpoints, Phase 5 e2e-fix verification gate, and tasks.md update protocol. |
agent-browser |
browser, automation, web |
Headless browser automation via the agent-browser CLI. Covers open, snapshot, click, fill, screenshot, and multi-session workflows. |
dogfood |
qa, testing, browser |
Systematic exploratory testing of a web app to find bugs and UX issues. Produces a structured report with screenshots and repro steps. |
e2e-fix |
qa, testing, fix, browser |
Run an end-to-end user flow, find bugs, fix them in code, re-run to verify. Loops until all issues are resolved. Used as the Phase 5 verification gate in spec-driven development. |
electron |
browser, automation, desktop |
Automate Electron desktop apps (VS Code, Slack, Discord, etc.) via Chrome DevTools Protocol. |
slack |
browser, automation, slack |
Interact with Slack workspaces using browser automation: read channels, send messages, search conversations. |
vercel-sandbox |
browser, automation, vercel |
Run agent-browser and Chrome inside Vercel Sandbox microVMs for browser automation from Vercel-deployed apps. |
Load a built-in skill inside an agent prompt with:
GetSkill { "name": "spec-driven-development" }
GetSkill { "name": "e2e-fix" }
Multi-spec decomposition: when a request involves 2+ distinct features, the agent creates one spec per feature and runs each through the full five-phase pipeline, including the Phase 5 e2e-fix verification gate, before moving to the next spec. See the system prompt's "Multi-Feature Decomposition & E2E Verification" section.
A user-provided or plugin skill with the same name as a built-in always takes
precedence — built-ins are inserted with insert_if_absent and are never
overwritten.
- Create
crates/qcr-core/src/prompts/skill.md(or add a new file for the skill body). - Add an
include_str!constant incrates/qcr-core/src/prompts/mod.rs. - Call
self.insert_if_absent(Skill { ... })insideSkillManager::register_builtin_skills()incrates/qcr-core/src/skills/mod.rs. - Add unit tests: happy-path load, precedence (user skill wins), appears in
list().
Each .md or .json file in agents/ defines a sub-agent the plugin
contributes. The LLM spawns plugin agents via the Task tool's agent
parameter:
{ "task": "Review auth.rs for security issues", "agent": "reviewer" }When the agent parameter is provided, the named plugin agent's
system_prompt, tools restriction, and max_turns are applied to
the sub-agent. An explicit tools parameter on the Task call overrides
the agent's tool list.
---
name: reviewer
description: Thorough code reviewer focused on correctness and style
model: qwen3-coder
tools: [ReadFile, Grep, Glob]
max_turns: 10
---
Review the provided code for bugs, performance issues, and style violations.
Return a structured report with severity levels.The body (everything after the --- closing fence) becomes the agent's
system_prompt. All frontmatter fields except name are optional.
{
"name": "reviewer",
"description": "Thorough code reviewer",
"system_prompt": "Review the provided code for bugs...",
"model": "qwen3-coder",
"tools": ["ReadFile", "Grep", "Glob"],
"max_turns": 10
}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique agent name (used in Task tool's agent parameter) |
description |
string | No | Human-readable description |
system_prompt |
string | No | System prompt injected when this agent is spawned (body in .md, field in .json) |
model |
string | No | Model override (reserved for future use; currently uses session default) |
tools |
array | No | Tool allow-list. When set, the sub-agent can only use these tools. When absent, all tools are available. |
max_turns |
integer | No | Max agent loop turns. When absent, uses the default (20). |
When two plugins define an agent with the same name, the first registered agent wins (global plugins load before project plugins within each scope, sorted by directory name). A warning is logged for collisions.
A plugin can declare additional MCP servers in .mcp.json. The format
matches the mcp_servers section of settings.json:
{
"lint-server": {
"transport": "stdio",
"command": "/usr/local/bin/lint-mcp",
"args": ["--config", ".lintrc"],
"auto_approve": false
},
"remote-checker": {
"transport": "http",
"url": "http://localhost:9100/mcp",
"auto_approve": true
}
}Qwen Code Rust namespaces each server by plugin name to avoid collisions:
lint-server from plugin feature-dev becomes feature-dev.lint-server.
Hooks let a plugin (or the project itself) run shell commands in response to lifecycle events in the agent loop. The hook format is compatible with Claude Code.
Hooks are configured in hooks/hooks.json inside the plugin directory.
Project-level hooks (not inside any plugin) go in .qcr/hooks/hooks.json.
{
"description": "Security and quality gates",
"hooks": {
"PreToolUse": [
{
"matcher": "Shell|WriteFile|EditFile",
"hooks": [
{
"type": "command",
"command": "${QCR_PLUGIN_ROOT}/hooks/security-check.sh",
"timeout": 15
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/inject-context.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "WriteFile|EditFile",
"hooks": [
{
"type": "command",
"command": "cargo fmt --check",
"async": true,
"timeout": 30
}
]
}
]
}
}| Event | Fires when | Blockable |
|---|---|---|
SessionStart |
A new session begins | No |
SessionEnd |
A session terminates | No |
UserPromptSubmit |
The user submits a prompt, before the agent processes it | Yes |
PreToolUse |
Before any tool executes | Yes |
PostToolUse |
After a tool call succeeds | No |
PostToolUseFailure |
After a tool call fails | No |
Stop |
The agent loop exits naturally | Yes |
SubagentStart |
A sub-agent is spawned via the Task tool | No |
SubagentStop |
A sub-agent finishes | Yes |
PreCompact |
Before context compaction | No |
Notification |
A notification is emitted | No |
TeammateIdle |
A teammate agent has no pending tasks and is waiting | Yes |
TaskCompleted |
A teammate completes a task via the CompleteTask tool |
Yes |
Blockable events: if a hook exits with code 2, qcr treats it as a
block — the triggering action is cancelled and an error message is shown.
For non-blockable events, exit code 2 is a warning and execution continues.
The optional matcher field is a pipe-separated list of tool names. Omit it
(or use "*") to match all tools.
{ "matcher": "Shell" } // Shell only
{ "matcher": "WriteFile|EditFile" } // write tools only
{ "matcher": "*" } // all tools (same as omitting matcher)| Field | Type | Default | Description |
|---|---|---|---|
type |
string | required | "command" (shell) or "markdown" (inject context) |
command |
string | — | Shell command to run (required for type: "command") |
content |
string | — | Markdown to inject (required for type: "markdown") |
timeout |
integer | 30 |
Timeout in seconds before the hook is killed |
async |
bool | false |
Run in the background without blocking the agent |
Use ${QCR_PLUGIN_ROOT} in a command string to reference the plugin's
root directory. This is replaced at runtime with the absolute path to the
plugin directory, allowing hooks to call scripts bundled with the plugin
regardless of the working directory.
{ "command": "${QCR_PLUGIN_ROOT}/hooks/check.sh" }Every hook receives a JSON object on stdin describing the current event:
{
"session_id": "session-1234567890",
"cwd": "/home/user/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Shell",
"tool_input": { "command": "rm -rf /tmp/build" }
}Common fields:
| Field | Description |
|---|---|
session_id |
Unique identifier for this session |
cwd |
Current working directory |
permission_mode |
Approval mode (default, plan, etc.) |
hook_event_name |
Name of the event that fired |
tool_name |
Set for PreToolUse, PostToolUse, PostToolUseFailure |
tool_input |
Tool input JSON — set for PreToolUse, PostToolUse |
tool_response |
Tool output text — set for PostToolUse |
error |
Error message — set for PostToolUseFailure |
prompt |
User prompt text — set for UserPromptSubmit |
last_assistant_message |
Last assistant message — set for Stop, SubagentStop |
teammate_name |
Teammate display name — set for TeammateIdle |
task_id |
Task ID — set for TaskCompleted |
task_title |
Task title — set for TaskCompleted |
Exit codes:
| Exit code | Meaning |
|---|---|
0 |
Success. Stdout is optionally parsed as a JSON decision. |
2 |
Block (blockable events only). The triggering action is cancelled. |
| Any other | Non-fatal warning. Execution continues. |
JSON stdout (exit 0):
A hook can return a JSON object on stdout to influence qcr's behaviour:
{
"continue": false,
"stopReason": "Build failed — fix errors before proceeding"
}| Field | Type | Description |
|---|---|---|
continue |
bool | Set false to stop the agent after this event |
stopReason |
string | Message shown when continue is false |
hookSpecificOutput.additionalContext |
string | Markdown injected as context into the conversation |
hookSpecificOutput.permissionDecision |
string | "allow" or "deny" for PreToolUse hooks |
hookSpecificOutput.permissionDecisionReason |
string | Reason shown when denying a tool call |
Example — deny a tool call from a PreToolUse hook:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Shell commands are not allowed in this project"
}
}Example — inject context from a SessionStart hook:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "## Project Context\nThis is a Rust project using tokio."
}
}Set "async": true on a handler to run it in the background. The agent
does not wait for it to finish. Async hooks cannot block events or inject
context — they are fire-and-forget for side effects like running tests or
sending notifications.
{
"type": "command",
"command": "cargo test --quiet 2>&1 | tee /tmp/test-results.log",
"async": true,
"timeout": 120
}The "markdown" handler type is a qcr extension. It injects static
markdown text as context into the conversation without running any process.
Useful for injecting project documentation or instructions at SessionStart.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "markdown",
"content": "## Coding Standards\nAlways run `cargo fmt` and `cargo clippy` before committing."
}
]
}
]
}
}Example — react when a teammate goes idle (TeammateIdle):
{
"hooks": {
"TeammateIdle": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Teammate $TEAMMATE_NAME is idle\" >> /tmp/team-status.log"
}
]
}
]
}
}TEAMMATE_NAME is set in the hook environment from HookContext.teammate_name.
Example — log task completions and optionally stop the agent (TaskCompleted):
{
"hooks": {
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.qcr/hooks/on_task_complete.py"
}
]
}
]
}
}The hook script receives TASK_ID and TASK_TITLE as environment variables. It can return:
{ "continue": false, "stopReason": "All tasks done — shutting down." }to stop the agent once all tasks are finished.
Hooks outside any plugin go in .qcr/hooks/hooks.json. They follow the
same format as plugin hooks but are not bundled with a plugin — they are
project-specific overrides.
.qcr/
├── hooks/
│ └── hooks.json ← project-level hooks
├── plugins/ ← project-local plugins
│ └── my-plugin/
└── settings.json
Project-level hooks are loaded under the source name "project" and fire
alongside plugin hooks for the same event.
mkdir -p ~/.qcr/plugins/hello/.claude-plugin
cat > ~/.qcr/plugins/hello/.claude-plugin/plugin.json <<'EOF'
{
"name": "hello",
"version": "1.0.0",
"description": "Example plugin"
}
EOFmkdir -p ~/.qcr/plugins/hello/commands
cat > ~/.qcr/plugins/hello/commands/greet.md <<'EOF'
Greet the user warmly. Their name is: $ARGUMENTS
Write a short, friendly greeting addressing them by name.
EOFInvoke with: /greet Alice
mkdir -p ~/.qcr/plugins/hello/hooks
cat > ~/.qcr/plugins/hello/hooks/hooks.json <<'EOF'
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "markdown",
"content": "You are working inside the hello plugin context."
}
]
}
],
"PreToolUse": [
{
"matcher": "Shell",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"Shell command approved\"}}'"
}
]
}
]
}
}
EOFmkdir -p ~/.qcr/plugins/hello/skills
cat > ~/.qcr/plugins/hello/skills/greet-style.md <<'EOF'
---
name: greet-style
description: How to greet users in this project
tags: [style, greet]
---
Always greet users with their preferred name. Use a warm, professional tone.
EOFOn startup, qcr:
- Scans
~/.qcr/plugins/(global scope) then.qcr/plugins/(project scope). - Treats each immediate subdirectory as a candidate plugin.
- Loads the plugin if it contains
.claude-plugin/plugin.jsonor.qcr-plugin/plugin.json. - Directories without a valid
plugin.jsonare silently skipped (not an error). - Deduplicates by name: project plugins shadow global plugins with the same name.
- Registers all plugin components: skills into
SkillManager, commands intoPluginCommandRegistry, hooks intoHookRunner, MCP servers into the MCP client manager. - Fires
SessionStarthooks; any injected markdown is shown as system context.
Plugin loading is best-effort. A plugin that fails to load (malformed JSON, missing required field) does not prevent other plugins from loading.
Load failures are recorded internally and can be surfaced via
qcr plugin list (when that subcommand is available).
Hooks execute arbitrary shell commands with the same permissions as qcr itself. Only install plugins from sources you trust.
- Do not add plugins that run untrusted scripts.
- Review
hooks/hooks.jsonand any hook scripts before installing a plugin. - Use
PreToolUsehooks with exit-code2blocking to enforce project-level policies, not as a security boundary. ${QCR_PLUGIN_ROOT}substitution is the only variable expansion performed in command strings — shell variable expansion happens inside the executed command itself, not during substitution.