Importing a Claude Code or Codex plugin
This page takes two small plugins, one written in Claude Code format with a
.claude-plugin/plugin.json manifest and one written in Codex format with a
.codex-plugin/plugin.json manifest, and installs each into Muse Code
without changing a file. You will see what the validator imports from each
format, what it warns about, how the shell-string hooks are translated, and
what an author used to either host has to do differently. Every command and
output below was run against Muse Code 1.3.0 with an empty plugin store. The
plugin surface is part of the Developer Preview and may change.
When to import instead of converting
Section titled “When to import instead of converting”Import when the package already exists and you want to keep using it as published: a plugin you maintain for Claude Code or Codex, or one you install from a marketplace that ships packages in one of those formats. Muse Code reads both layouts directly, so there is nothing to translate and the same directory keeps working in every host.
Convert to the native format when you start depending on Muse Code features
the other formats cannot express: an argv hook command that runs without a
shell, a statusMessage on a native hook entry, an HTTP MCP server, or a
stable hook id you choose yourself. The last section shows the conversion.
For the full matrix of what each format can carry, read
Compatibility with other plugin formats.
How the format is recognised
Section titled “How the format is recognised”Muse Code identifies a package by one marker file and parses it with the adapter for that format.
| Format | Marker | Label in validate output |
Imported |
|---|---|---|---|
| Claude Code | .claude-plugin/plugin.json |
claude-compatible |
Skills, commands, hooks, stdio MCP servers |
| Codex | .codex-plugin/plugin.json |
codex-compatible |
Skills, hooks, stdio MCP servers |
A manifest under either directory goes through the foreign adapter only
when it has none of the native keys schemaVersion, compat and
capabilities. When a package carries both markers, Muse Code picks
.claude-plugin/ over .codex-plugin/, ignores the other without parsing
it, and prints one multiple-manifests warning; a native .muse-plugin/
marker beats both. Agent Plugins 1.0.0 packages, which use a root
plugin.json, are covered on the compatibility page.
The Claude Code plugin
Section titled “The Claude Code plugin”notes-claude/├── .claude-plugin/│ └── plugin.json├── skills/│ └── meeting-notes/│ └── SKILL.md├── commands/│ └── summarize-notes.md└── hooks/ ├── hooks.json └── record-write.shThe manifest carries only metadata. Skills, commands and hooks are found by
convention: skills/**/SKILL.md, commands/*.md and hooks/hooks.json.
{ "name": "notes-claude", "description": "Example Claude Code plugin: a meeting-notes skill, a summarize-notes command, and a PostToolUse hook that records which files were written.", "version": "0.1.0", "author": { "name": "Muse Code documentation" }}---name: meeting-notesdescription: Turn raw meeting notes into a structured summary with decisions, action items and open questions. Use when the user pastes notes or a transcript from a meeting and wants them cleaned up.---
# Meeting notes
Turn the raw notes the user gives you into a summary that someone who missedthe meeting can read in a minute. Do not fetch anything; work only from thetext in the conversation.
## Output shape
1. **Summary**: two or three sentences on what the meeting was about and what changed.2. **Decisions**: one bullet per decision, phrased as a fact ("The launch moves to 12 May").3. **Action items**: one bullet per item, in the form `owner: task (due date if given)`. If no owner was named, write `unassigned`.4. **Open questions**: anything raised but not settled.
Keep the original wording for names, dates and numbers. If the notes areambiguous about who owns an item, say so rather than guessing.The command uses the Claude Code front matter keys, including
allowed-tools, which Muse Code accepts but does not enforce.
---description: Summarize the meeting notes pasted after the commandargument-hint: <notes or a file path>allowed-tools: Read---Summarize these meeting notes using the meeting-notes skill:
$ARGUMENTS
If the argument is a file path rather than the notes themselves, read the filefirst. Keep the summary under 200 words.The hook file uses the Claude Code group and handler shape: an event name, a
group with a matcher, and a handler whose command is a shell string. The
path is quoted because the installed copy lives under a cache path you do not
control.
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/record-write.sh\"", "timeout": 10 } ] } ] }}#!/bin/sh# PostToolUse hook: append the path of every file the model wrote or edited# to a log in the plugin's data directory. Never blocks.set -u
payload=$(cat)
# Muse Code sets MUSE_PLUGIN_DATA_DIR and the alias CLAUDE_PLUGIN_DATA with the# same value. Create the directory: Muse Code advertises it but does not# create it.data_dir="${MUSE_PLUGIN_DATA_DIR:-${CLAUDE_PLUGIN_DATA:-}}"[ -n "$data_dir" ] || exit 0mkdir -p "$data_dir" || exit 0
tool=$(printf '%s' "$payload" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)path=$(printf '%s' "$payload" | sed -n 's/.*"\(file_\)\{0,1\}path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\2/p' | head -n 1)
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${tool:-unknown}" "${path:-?}" >> "$data_dir/writes.log"exit 0Make the script executable:
chmod +x notes-claude/hooks/record-write.shThe command runs the file directly and the execute bit is preserved when the
package is copied into the plugin cache. If you forget it, the package digest
is unchanged but hook test reports status=failed with exit_code 126 and
permission denied on stderr.
The Codex plugin
Section titled “The Codex plugin”notes-codex/├── .codex-plugin/│ └── plugin.json├── skills/│ └── meeting-notes/│ └── SKILL.md└── hooks/ ├── hooks.json └── record-write.shThe Codex format has no commands, so this package is the same idea without
commands/. Its manifest names the skills directory explicitly: in a Codex
manifest skills is one string path to a directory that holds at least one
SKILL.md, and there is no conventional scan when the key is absent. Hooks
are read from hooks/hooks.json when the manifest has no hooks key.
{ "name": "notes-codex", "version": "0.1.0", "description": "Example Codex plugin: a meeting-notes skill and a PostToolUse hook that records which files were written.", "skills": "skills"}The hook file has the same group and handler shape as the Claude Code one.
The only difference is the placeholder: ${PLUGIN_ROOT} is the name a Codex
author writes, and Muse Code provides it as an alias of MUSE_PLUGIN_ROOT.
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"${PLUGIN_ROOT}/hooks/record-write.sh\"", "timeout": 10 } ] } ] }}notes-codex/skills/meeting-notes/SKILL.md is byte for byte the file shown
above for the Claude Code plugin. notes-codex/hooks/record-write.sh is the
same script with one line changed, so that it reads the Codex alias instead
of the Claude Code one:
data_dir="${MUSE_PLUGIN_DATA_DIR:-${PLUGIN_DATA:-}}"Either alias would work in either package, because Muse Code sets all of them; the scripts use the name their original host documents. Make this script executable too:
chmod +x notes-codex/hooks/record-write.shWhat Muse Code takes from each package
Section titled “What Muse Code takes from each package”From the Claude Code package Muse Code imports the metadata (name,
version, description), the skill under skills/, the command under
commands/ and the hook group in hooks/hooks.json. The author field is
presentation only and produces a warning, and the command’s allowed-tools
is checked for syntax but grants nothing. A .mcp.json would be imported as
well, as long as every server is stdio, has no env and uses only the plugin
root and data placeholders; agents, lspServers, outputStyles,
settings and userConfig stay inactive, and dependencies or
requiredPlugins block the install.
From the Codex package Muse Code imports the metadata, every SKILL.md
under the directory the skills key names, and the hook group in
hooks/hooks.json. MCP servers follow the same stdio-only, no-env rules as
Claude Code. There are no commands to import. Four hook events that Codex
defines are refused (Notification, PostToolUseFailure, StopFailure and
PostToolBatch), apps is listed as unsupported and stays inactive, and a
manifest that declares neither skills nor an MCP server is rejected
because there is nothing to import. The complete matrix for both formats is
in Compatibility with other plugin formats.
Plugin MCP server entries have no env, headers or credentials in any
format.
Placeholder variables
Section titled “Placeholder variables”Imported hook commands are shell strings and Muse Code passes them to a shell
unchanged. ${CLAUDE_PLUGIN_ROOT} and ${PLUGIN_ROOT} in the commands above
are expanded by the shell from the environment Muse Code gives every plugin
process: MUSE_PLUGIN_ID, MUSE_PLUGIN_ROOT (the read-only installed copy)
and MUSE_PLUGIN_DATA_DIR (a writable per-plugin directory), plus the
aliases CLAUDE_PLUGIN_ROOT and PLUGIN_ROOT for the root and
CLAUDE_PLUGIN_DATA and PLUGIN_DATA for the data directory. The
MUSE_PLUGIN_DATA_DIR directory may not exist yet, so create it before
writing. Hook events and payloads lists the
whole environment.
Imported MCP server entries are different: their command and args are
not run through a shell, so Muse Code substitutes ${CLAUDE_PLUGIN_ROOT},
${PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA} and ${PLUGIN_DATA} itself, once,
before launch. Any other ${...} token in an MCP entry rejects that server.
Native manifests get neither: native hook and MCP commands get no placeholder
substitution, Muse Code rewrites exactly one argv element, the one that
spells a relative path declared in the manifest, to the installed copy, and
scripts read MUSE_PLUGIN_ROOT for anything else.
Validate
Section titled “Validate”Validate the Claude Code package first:
muse plugins validate notes-claudediagnostic=unsupported-capability severity=warning path=notes-claude/commands/summarize-notes.md message=Claude command declares `allowed-tools`, which is not enforced yet; the command runs under ordinary approval and no tool permission is granteddiagnostic=unsupported-field severity=warning path=notes-claude/.claude-plugin/plugin.json message=Claude manifest field `author` is presentation-only and is not importedvalid notes-claude claude-compatible skills=1 commands=1 hooks=1 mcp=0 reminders=0 diagnostics=2The family is claude-compatible, all three capabilities were found, and the
two warnings are the two things a Claude Code author should expect:
author is not imported, and allowed-tools grants nothing. Paths are
shortened here; the real output prints them in full.
The Codex package validates clean:
muse plugins validate notes-codexvalid notes-codex codex-compatible skills=1 commands=0 hooks=1 mcp=0 reminders=0 diagnostics=0The JSON form shows the compatibility summary, the declarations, and how the hook was translated. For the Claude Code package, trimmed to those parts:
muse plugins validate notes-claude --json{ "valid": true, "plugin": { "id": "notes-claude", "version": "0.1.0", "manifest_family": "claude-compatible", "compatibility": { "summary": "partial", "declarations": [ { "id": "skill:meeting-notes", "kind": "skill", "classification": "supported" }, { "id": "hook:hook-0ef08c6de5588a30", "kind": "hook", "classification": "supported" }, { "id": "command:summarize-notes", "kind": "command", "classification": "supported" }, { "id": "invocation-preapproval:command:summarize-notes", "kind": "invocation-preapproval", "classification": "unsupported" } ] } }, "capabilities": { "hooks": [ { "id": "hook-0ef08c6de5588a30", "event": "PostToolUse", "matcher": "Write|Edit", "command": ["\"${CLAUDE_PLUGIN_ROOT}/hooks/record-write.sh\""], "shell_command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/record-write.sh\"", "source_relative_path": "hooks/hooks.json", "timeout_ms": 10000, "async": false, "compatibility_name": null } ] }}Read the hook entry against the hooks.json you wrote:
idis generated:hook-followed by 16 hex digits derived from the source file, the event and the handler definition. You did not choose it, and it changes if you change the command, matcher, timeout or any other handler field. You will use it in every later command.shell_commandis your command string, untouched.commandrepeats it as a one-element array for tools that read the native shape.matcheris kept, and it is applied at run time.timeout: 10(seconds) becametimeout_ms: 10000. Without atimeoutthe hook gets 600 seconds.
summary is partial, not full, because of the
invocation-preapproval:command:summarize-notes row: that is the
allowed-tools declaration, recorded as unsupported. A partial package
installs; only an unsupported summary, or a rejected MCP server or
dependency, blocks the install.
The Codex package’s JSON has the same shape with two declarations, both
supported, and a full summary. Its hook id is hook-7488bef2c6b21737;
the shell_command is the ${PLUGIN_ROOT} string from its hooks.json,
and timeout_ms is again 10000:
muse plugins validate notes-codex --json{ "valid": true, "plugin": { "id": "notes-codex", "version": "0.1.0", "manifest_family": "codex-compatible", "compatibility": { "summary": "full", "declarations": [ { "id": "skill:meeting-notes", "kind": "skill", "classification": "supported" }, { "id": "hook:hook-7488bef2c6b21737", "kind": "hook", "classification": "supported" } ] } }, "diagnostics": []}Install and list
Section titled “Install and list”muse plugins install notes-claudemuse plugins install notes-codexinstalled notes-claude 0.1.0 enabled=true trust=user-local provenance=foreign-import cache=<data dir>/plugins/cache/local/notes-claude/8cd6a453…/packagewarning third-party plugin: hooks require review before activation; skills and commands are active without review while the plugin is enabledinstalled notes-codex 0.1.0 enabled=true trust=user-local provenance=foreign-import cache=<data dir>/plugins/cache/local/notes-codex/8183c161…/packagewarning third-party plugin: hooks require review before activation; skills are active without review while the plugin is enabledThe Claude Code install prints its two validation diagnostics first, in a
slightly longer form that adds kind= and id= fields; they are omitted
here. provenance=foreign-import marks a package that came through a
compatibility adapter; a native package shows native-local. The warning
line is tailored to what each package carries: the Codex plugin has no
commands, so its warning names skills alone. Install scope is the user
store, recorded in your own configuration; it is separate from capability
review.
muse plugins listnotes-claude 0.1.0 enabled=true active=true trust=user-local provenance=foreign-import valid=true diagnostics=2warning third-party plugin: hooks require review before activation; skills and commands are active without review while the plugin is enablednotes-codex 0.1.0 enabled=true active=true trust=user-local provenance=foreign-import valid=true diagnostics=0warning third-party plugin: hooks require review before activation; skills are active without review while the plugin is enabledIn muse plugins list --json each record carries its manifest_family,
claude-compatible or codex-compatible, next to
"provenance": "foreign-import". The skills and the command are live now.
In a Muse Code session, /summarize-notes <notes> expands the Claude Code
command’s template, and /meeting-notes invokes whichever meeting-notes
skill was registered first; the qualified forms
/notes-claude:meeting-notes, /notes-codex:meeting-notes and
/notes-claude:summarize-notes always name one plugin’s capability. A
session that was already open picks them up on your next prompt.
Inspect: what needs review
Section titled “Inspect: what needs review”muse plugins inspect notes-claudenotes-claude 0.1.0 enabled=true active=true trust=user-local valid=true skills=1 commands=1 hooks=1 mcp=0 reminders=0 cache=<data dir>/plugins/cache/local/notes-claude/8cd6a453…/packagewarning third-party plugin: hooks require review before activation; skills and commands are active without review while the plugin is enabledruntime-capability plugin:notes-claude:hook:hook-0ef08c6de5588a30 status=review_neededcapability-diagnostic agent_definition_review_unavailable agent definitions are not reviewable and stay inactive: inventory_refresh_requiredThe hook is the only capability waiting for review. The last line is printed for this imported plugin even though it ships no agent files; there is nothing to do about it.
muse plugins inspect notes-codexnotes-codex 0.1.0 enabled=true active=true trust=user-local valid=true skills=1 commands=0 hooks=1 mcp=0 reminders=0 cache=<data dir>/plugins/cache/local/notes-codex/8183c161…/packagewarning third-party plugin: hooks require review before activation; skills are active without review while the plugin is enabledruntime-capability plugin:notes-codex:hook:hook-7488bef2c6b21737 status=review_neededThe JSON form separates what is already effective from what is waiting:
muse plugins inspect notes-claude --json{ "effective_capabilities": [ { "kind": "command", "stable_id": "plugin:notes-claude:summarize-notes" }, { "kind": "skill", "stable_id": "plugin:notes-claude:meeting-notes" } ], "runtime_capabilities": [ { "candidate": { "kind": "hook", "plugin_id": "notes-claude", "capability_id": "hook-0ef08c6de5588a30", "stable_id": "plugin:notes-claude:hook:hook-0ef08c6de5588a30", "definition_hash": "sha256:9aaf4c03…", "source_digest": "sha256:3778687b…" }, "status": "review_needed", "diagnostic": null } ]}Approve and test the hooks
Section titled “Approve and test the hooks”Approve one hook by its selector <plugin-id>:hook:<hook-id>, or approve
everything reviewable in a plugin by naming the plugin. Both forms are shown,
one per package:
muse plugins approve notes-claude:hook:hook-0ef08c6de5588a30 --json{ "decision": "approve", "runtime_capabilities": [ { "stable_id": "plugin:notes-claude:hook:hook-0ef08c6de5588a30", "trusted_definition_hash": "sha256:9aaf4c03…", "enabled": true } ]}muse plugins approve notes-codex --json{ "decision": "approve", "runtime_capabilities": [ { "stable_id": "plugin:notes-codex:hook:hook-7488bef2c6b21737", "trusted_definition_hash": "sha256:8c73e80d…", "enabled": true } ]}muse plugins inspect now prints status=trusted_enabled for each hook.
The approval is bound to trusted_definition_hash, which covers the handler
definition and the installed package, so any later change to a package
returns its hook to modified and it waits for review again. Hooks are
wired at session start: start a new Muse Code session and every completed
write or edit appends one line to writes.log in each plugin’s data
directory.
To run a hook without a session, write a fixture. matcher_input is the
tool name the matcher is checked against; in a session Muse Code supplies it
from the tool call.
{ "event": "PostToolUse", "matcher_input": "write_file", "stdin": { "hook_event_name": "PostToolUse", "session_id": "test-session", "cwd": "/tmp", "tool_name": "write_file", "tool_input": { "path": "notes/2026-09-17.md", "content": "# Standup" }, "tool_response": { "ok": true } }}muse plugins hook test notes-claude:hook-0ef08c6de5588a30 --fixture fixture.json --json{ "decision": { "should_block": false, "should_stop": false }, "terminals": [ { "hook_key": "plugin:notes-claude:hook-0ef08c6de5588a30", "event": "post_tool_use", "status": "completed", "duration_ms": 721, "exit_code": 0, "stdout": "", "stderr": "" } ], "records": 2}The output is trimmed to the fields worth reading. status is completed:
the shell expanded ${CLAUDE_PLUGIN_ROOT}, found the installed script, and
the script exited 0. The same fixture works for the Codex hook, whose
command spells ${PLUGIN_ROOT} instead; the one-line form is enough to see
it run:
muse plugins hook test notes-codex:hook-7488bef2c6b21737 --fixture fixture.jsonhook-test notes-codex:hook-7488bef2c6b21737 status=completedEach plugin’s log now has one line:
2026-09-17T17:30:14Z write_file notes/2026-09-17.mdThe matcher Write|Edit matched the tool name write_file. Muse Code maps
the Claude Code and Codex tool names to its own: Write to write_file,
Edit to edit_file, Read to read_file, Bash to bash, Grep to
search, WebFetch to web_fetch and WebSearch to web_search. To see
the matcher exclude a call, copy the fixture with matcher_input and
tool_name set to read_file and run it again against either hook:
{ "decision": { "should_block": false, "should_stop": false }, "terminals": [], "records": 0}No terminal means the hook was not selected, so the script did not run. The
one-line form prints status=unknown in that case.
Differences a Claude Code author will notice
Section titled “Differences a Claude Code author will notice”| In Claude Code | In Muse Code |
|---|---|
| Hooks and MCP servers run once the plugin is enabled. | Hooks and MCP servers stay inactive until muse plugins approve, and load in the next session. Skills and commands are live on install. |
| Any package edit keeps the hook active. | Any package change returns an approved hook to modified; approve it again. Changing the handler also changes its generated id. |
| You name nothing about a hook. | The hook gets a generated id, hook-<16 hex>, which you use in approve, reject and hook test. |
matcher selects tools by Claude Code tool names. |
matcher is kept and honoured for imported hooks, and Claude Code tool names are mapped to Muse Code tool names. Native hooks have no matcher. |
| The command is a shell string. | Imported hook commands stay shell strings and run through a shell. Native hook commands are argv arrays and run without one. |
| Event names in PascalCase. | The same names, plus PreUserTurn as an alias of UserPromptSubmit. Setup is recognised and skipped with a warning; do any setup by hand. |
timeout in seconds. |
The same, converted to milliseconds; default 600 seconds. |
allowed-tools pre-approves tools for a command. |
Parsed and validated, not enforced. The command runs under ordinary approval, and the package summary reads partial. |
disable-model-invocation: true on a command. |
Honoured: the command is user-invocable only. |
${CLAUDE_PLUGIN_ROOT} in hooks and MCP entries. |
Works in both. Hooks get it from the environment; MCP commands and args have it substituted before launch. |
env on MCP servers, userConfig, agents, lspServers, outputStyles, settings. |
An MCP server with env blocks the install; the rest are inactive. dependencies and requiredPlugins block the install. |
/summarize-notes only. |
/summarize-notes when the name is free, and always /notes-claude:summarize-notes. |
Differences a Codex author will notice
Section titled “Differences a Codex author will notice”Everything in the table above about review, generated hook ids, shell-string commands, timeouts and placeholders applies to a Codex package too. These rows are specific to the Codex format:
| In Codex | In Muse Code |
|---|---|
skills points at a directory. |
The same: skills must be one string path to a directory with at least one SKILL.md, searched up to 16 levels deep; each skill’s id is its parent directory name. Without the key no skills are imported. |
name is optional. |
Optional; it defaults to the package directory name. version defaults to 0.0.0. |
Hooks in hooks/hooks.json or under a hooks key. |
Both are read, with the same group and handler shape as Claude Code but without an if selector. Notification, PostToolUseFailure, StopFailure and PostToolBatch fail validation with unsupported-hook-event. |
${PLUGIN_ROOT} and ${PLUGIN_DATA} in commands. |
Provided as aliases of MUSE_PLUGIN_ROOT and MUSE_PLUGIN_DATA_DIR, alongside the Claude Code names. |
| Commands. | Not part of the Codex format; none are imported. Put a prompt template in a skill instead. |
apps, tools, agents, outputStyles, settings, developerPrompts. |
Listed as unsupported declarations; they install but stay inactive. |
| A manifest with only metadata and hooks. | Rejected: a Codex package must declare skills or an MCP server to be importable. |
/meeting-notes only. |
/meeting-notes when the name is free, and always /notes-codex:meeting-notes. |
For the payload each event receives and the exit codes that block, read Hook events and payloads.
Common mistakes
Section titled “Common mistakes”Each output below is real. Paths are shortened.
| Symptom | Cause | Fix |
|---|---|---|
invalid notes-codex-badevent diagnostics=1 followed by diagnostic=unsupported-hook-event severity=error path=notes-codex-badevent/.codex-plugin/plugin.json message=foreign hook event `Notification` is unsupported |
A Codex hooks.json registers one of the four events Muse Code refuses from that format. The diagnostic is attributed to the manifest even though the event is in the hook file. |
Remove that event group or move the behaviour to an event Muse Code runs, such as PostToolUse or Stop. |
diagnostic=invalid-manifest-schema severity=error path=codex-empty/.codex-plugin/plugin.json message=this Codex plugin declares no importable skills yet (hooks/MCP/commands are not imported in this phase) |
The Codex manifest has neither a skills key nor an MCP server. Hooks alone do not make a Codex package importable, whatever the message’s wording suggests. |
Add "skills": "<dir>" pointing at a directory with a SKILL.md, or an MCP server. |
validate prints diagnostic=unsupported-capability severity=warning path=… message=Codex MCP server `api` rejected: non-empty-env and reports valid, but install prints plugin declares a retained unsupported MCP or dependency boundary and exits 1 |
An MCP server in the package carries env. The same happens for a Claude Code .mcp.json server with env, a non-stdio transport or an unknown placeholder. |
Move the server to settings.json, where credentials are supported, or remove it from the package. |
diagnostic=multiple-manifests severity=warning path=both-markers message=selected `.claude-plugin/plugin.json`; ignoring `.codex-plugin/plugin.json` |
The directory ships both foreign markers. Muse Code reads the Claude Code manifest and never parses the Codex one. | Expected when one directory serves several hosts. Make sure the Claude Code manifest is the one you want Muse Code to see, or add a .muse-plugin/plugin.json, which wins over both. |
hook test reports status=failed, exit_code 126, permission denied on stderr |
The hook script is not executable. | chmod +x the script and reinstall; the package digest does not change. |
Convert to native later
Section titled “Convert to native later”You can keep a package in Claude Code or Codex format indefinitely. Convert when you want argv commands, a hook id you choose, or the other native-only fields. The conversion for the Claude Code plugin is one new manifest and one deleted file:
- Create
.muse-plugin/plugin.jsonand delete.claude-plugin/plugin.json. If both stay, Muse Code picks.muse-pluginand warns about the other. - Move each hook handler into the manifest as a native entry. The command
becomes an argv array with a relative path, and there is no
matcher. - Delete
hooks/hooks.json.
{ "schemaVersion": 1, "name": "notes-claude", "version": "0.2.0", "description": "Example plugin: a meeting-notes skill, a summarize-notes command, and a PostToolUse hook that records which files were written.", "compat": { "source": "native", "manifestDir": ".muse-plugin" }, "capabilities": { "skills": [ { "id": "meeting-notes", "path": "skills/meeting-notes/SKILL.md" } ], "commands": [ { "id": "summarize-notes", "path": "commands/summarize-notes.md" } ], "hooks": [ { "id": "record-write", "event": "PostToolUse", "command": ["sh", "hooks/record-write.sh"], "timeoutMs": 10000 } ] }}Muse Code rewrites hooks/record-write.sh in the argv to the installed
copy, so the script needs no ${CLAUDE_PLUGIN_ROOT}. The skill and command
files do not change. Validating this layout gives:
valid notes-claude native skills=1 commands=1 hooks=1 mcp=0 reminders=0 diagnostics=0Two things move from the manifest into the script:
-
The native hook runs on every
PostToolUse, because native hooks key on the event alone. Adding"matcher"to a native hook entry is an error (unsupported-field; the message says that native hooks key onevent). Filter inside the script instead, after thetool=line:Terminal window case "$tool" inwrite_file|edit_file) ;;*) exit 0 ;;esac -
allowed-toolsin the command front matter is no longer read at all, so remove it to keep the file honest.
The Codex plugin converts the same way: the manifest above without the
commands array, with "name": "notes-codex", and with the same explicit
skills entry, because a native manifest lists every skill instead of
scanning the directory the Codex skills key named.
After converting, remove the imported plugin and install the native one; the
hook id is now record-write, so approve it as
notes-claude:hook:record-write or notes-codex:hook:record-write.
Remove
Section titled “Remove”muse plugins remove notes-claude --delete-datamuse plugins remove notes-codex --delete-dataremoved notes-clauderemoved notes-codex--delete-data also deletes each plugin’s data directory, including
writes.log. Without it the data directory is kept for a future install.
muse plugins list now prints no plugins.
Next steps
Section titled “Next steps”- Compatibility with other plugin formats: the full import matrix for Claude Code, Codex and Agent Plugins packages.
- Hooks and Hook events and payloads: what a hook receives and how it blocks.
- Trust, review and scopes: why
hooks wait for approval and what
modifiedmeans. - Validation diagnostics: every code the validator can print, including the ones above.
- Quickstart: your first plugin: the same workflow with a native package.