Skip to content
Developer Preview

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.

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.

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.

notes-claude/
├── .claude-plugin/
│ └── plugin.json
├── skills/
│ └── meeting-notes/
│ └── SKILL.md
├── commands/
│ └── summarize-notes.md
└── hooks/
├── hooks.json
└── record-write.sh

The manifest carries only metadata. Skills, commands and hooks are found by convention: skills/**/SKILL.md, commands/*.md and hooks/hooks.json.

notes-claude/.claude-plugin/plugin.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"
}
}
notes-claude/skills/meeting-notes/SKILL.md
---
name: meeting-notes
description: 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 missed
the meeting can read in a minute. Do not fetch anything; work only from the
text 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 are
ambiguous 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.

notes-claude/commands/summarize-notes.md
---
description: Summarize the meeting notes pasted after the command
argument-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 file
first. 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.

notes-claude/hooks/hooks.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/record-write.sh\"",
"timeout": 10
}
]
}
]
}
}
notes-claude/hooks/record-write.sh
#!/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 0
mkdir -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 0

Make the script executable:

Terminal window
chmod +x notes-claude/hooks/record-write.sh

The 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.

notes-codex/
├── .codex-plugin/
│ └── plugin.json
├── skills/
│ └── meeting-notes/
│ └── SKILL.md
└── hooks/
├── hooks.json
└── record-write.sh

The 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.

notes-codex/.codex-plugin/plugin.json
{
"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.

notes-codex/hooks/hooks.json
{
"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:

notes-codex/hooks/record-write.sh (the changed line)
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:

Terminal window
chmod +x notes-codex/hooks/record-write.sh

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.

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 the Claude Code package first:

Terminal window
muse plugins validate notes-claude
diagnostic=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 granted
diagnostic=unsupported-field severity=warning path=notes-claude/.claude-plugin/plugin.json message=Claude manifest field `author` is presentation-only and is not imported
valid notes-claude claude-compatible skills=1 commands=1 hooks=1 mcp=0 reminders=0 diagnostics=2

The 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:

Terminal window
muse plugins validate notes-codex
valid notes-codex codex-compatible skills=1 commands=0 hooks=1 mcp=0 reminders=0 diagnostics=0

The JSON form shows the compatibility summary, the declarations, and how the hook was translated. For the Claude Code package, trimmed to those parts:

Terminal window
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:

  • id is 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_command is your command string, untouched. command repeats it as a one-element array for tools that read the native shape.
  • matcher is kept, and it is applied at run time.
  • timeout: 10 (seconds) became timeout_ms: 10000. Without a timeout the 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:

Terminal window
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": []
}
Terminal window
muse plugins install notes-claude
muse plugins install notes-codex
installed notes-claude 0.1.0 enabled=true trust=user-local provenance=foreign-import cache=<data dir>/plugins/cache/local/notes-claude/8cd6a453…/package
warning third-party plugin: hooks require review before activation; skills and commands are active without review while the plugin is enabled
installed notes-codex 0.1.0 enabled=true trust=user-local provenance=foreign-import cache=<data dir>/plugins/cache/local/notes-codex/8183c161…/package
warning third-party plugin: hooks require review before activation; skills are active without review while the plugin is enabled

The 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.

Terminal window
muse plugins list
notes-claude 0.1.0 enabled=true active=true trust=user-local provenance=foreign-import valid=true diagnostics=2
warning third-party plugin: hooks require review before activation; skills and commands are active without review while the plugin is enabled
notes-codex 0.1.0 enabled=true active=true trust=user-local provenance=foreign-import valid=true diagnostics=0
warning third-party plugin: hooks require review before activation; skills are active without review while the plugin is enabled

In 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.

Terminal window
muse plugins inspect notes-claude
notes-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…/package
warning third-party plugin: hooks require review before activation; skills and commands are active without review while the plugin is enabled
runtime-capability plugin:notes-claude:hook:hook-0ef08c6de5588a30 status=review_needed
capability-diagnostic agent_definition_review_unavailable agent definitions are not reviewable and stay inactive: inventory_refresh_required

The 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.

Terminal window
muse plugins inspect notes-codex
notes-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…/package
warning third-party plugin: hooks require review before activation; skills are active without review while the plugin is enabled
runtime-capability plugin:notes-codex:hook:hook-7488bef2c6b21737 status=review_needed

The JSON form separates what is already effective from what is waiting:

Terminal window
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 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:

Terminal window
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
}
]
}
Terminal window
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.

fixture.json
{
"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 }
}
}
Terminal window
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:

Terminal window
muse plugins hook test notes-codex:hook-7488bef2c6b21737 --fixture fixture.json
hook-test notes-codex:hook-7488bef2c6b21737 status=completed

Each plugin’s log now has one line:

2026-09-17T17:30:14Z write_file notes/2026-09-17.md

The 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.

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.

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.

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:

  1. Create .muse-plugin/plugin.json and delete .claude-plugin/plugin.json. If both stay, Muse Code picks .muse-plugin and warns about the other.
  2. 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.
  3. Delete hooks/hooks.json.
notes-claude/.muse-plugin/plugin.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=0

Two 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 on event). Filter inside the script instead, after the tool= line:

    Terminal window
    case "$tool" in
    write_file|edit_file) ;;
    *) exit 0 ;;
    esac
  • allowed-tools in 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.

Terminal window
muse plugins remove notes-claude --delete-data
muse plugins remove notes-codex --delete-data
removed notes-claude
removed 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.