{"page":"config","title":"userConfig and options","blurb":"Declaring options in plugin.json, what register() gets, and the three ways a config stops your mod loading.","build":"2.1.286","sections":["Declaring the fields","Three ways this goes wrong","Configuring one","What arrives at runtime","$.config is the \/config menu","Reading the person's actual settings"],"limit":8000,"section":null,"offset":0,"count":7561,"total":11589,"next_offset":7561,"next_url":"https:\/\/changelogs.core-directive.com\/reference\/mods\/config.json?offset=7561","markdown":"A mod gets its configuration as the second argument to `register`. You declare the fields in `plugin.json`, the person running your mod fills them in through their settings, and the engine hands you a frozen object. At 2.1.272 none of Anthropic's own mods used it, so their source won't teach you anything here, and the ways it goes wrong are worse than the shape suggests. I measured all of this at 2.1.272 and re-ran the failure cases at 2.1.283 and again at 2.1.286, where the schema hasn't changed.\n\n```ts\nexport function register(on: On, options: PluginOptions) {\n  const endpoint = options.endpoint\n\n  on('turn.complete', async ($, e, next) => { \/* ... *\/ })\n}\n```\n\n## Declaring the fields\n\n```json\n{\n  \"name\": \"my-mod\",\n  \"version\": \"0.0.1\",\n  \"description\": \"...\",\n  \"userConfig\": {\n    \"endpoint\": {\n      \"type\": \"string\",\n      \"title\": \"Endpoint\",\n      \"description\": \"Where to post the summary.\",\n      \"default\": \"https:\/\/example.test\/hook\"\n    },\n    \"timeoutMs\": {\n      \"type\": \"number\",\n      \"title\": \"Timeout\",\n      \"description\": \"How long to wait, in milliseconds.\",\n      \"default\": 2000\n    }\n  }\n}\n```\n\nThe object is strict. An unknown key anywhere in a field is refused with `userConfig.<name>: Invalid input` and nothing more, so a typo means bisecting your manifest.\n\nField names have to match `^[A-Za-z_]\\w*$`, since each one is also exported to your hooks' environment as `CLAUDE_PLUGIN_OPTION_` plus the name in capitals, so `apiUrl` becomes `CLAUDE_PLUGIN_OPTION_APIURL`.\n\n### Every field needs three keys\n\n`type`, `title` and `description`, on every field of every type. `title` isn't just for show, because it's the name the loader uses when it rejects a value. A field whose `title` doesn't match its key makes that error harder to trace.\n\n### Five types\n\n<table>\n<thead><tr><th>Accepted<\/th><th>Rejected<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>string<\/code><\/td><td rowspan=\"5\"><code>integer<\/code>, <code>float<\/code>, <code>select<\/code>, <code>multiselect<\/code>, <code>secret<\/code>, <code>password<\/code>, <code>enum<\/code>, <code>list<\/code>, <code>array<\/code>, <code>object<\/code>, <code>text<\/code>, <code>path<\/code><\/td><\/tr>\n<tr><td><code>number<\/code><\/td><\/tr>\n<tr><td><code>boolean<\/code><\/td><\/tr>\n<tr><td><code>file<\/code><\/td><\/tr>\n<tr><td><code>directory<\/code><\/td><\/tr>\n<\/tbody>\n<\/table>\n\nThere's no array type, so a list is a `string` with `\"multiple\": true`. There's no enum type either, and a fixed set of choices is a `string` with `options`.\n\n### Seven optional keys\n\n`default`, `required`, `sensitive`, `multiple`, `options`, `min` and `max`. The validator accepts all seven on all five types, which is looser than it sounds. `min` on a boolean passes and does nothing, since `min` and `max` only apply to numbers, and a backwards `{\"min\": 9, \"max\": 1}` goes through without a word.\n\nFour rules across fields are enforced, with messages clear enough to quote:\n\n```text\noptions is only for a field of type \"string\" that is neither multiple nor sensitive\noptions needs at least one value\ndefault must be one of the options: x, y\na field with options needs a default among them, or required: true\n```\n\nEach value in `options` has to be non-empty, 64 characters at most, with no control characters and no leading or trailing space, and no two can differ only by case.\n\n## Three ways this goes wrong\n\n### The validator doesn't check your default's type. The loader does.\n\nThis is the one to remember from this page. Declare a number field with a string default:\n\n```json\n{ \"type\": \"number\", \"title\": \"Bad\", \"description\": \"d\", \"default\": \"three\" }\n```\n\n`claude plugin validate` says, at 2.1.286:\n\n```text\n\u2714 Validation passed\n```\n\nIt used to say \"passed with warnings\", and the only warning was that I'd left out `author`. Add one and the bad default gets a clean pass. Then when it loads, the whole module is dropped, with the same text at 2.1.286:\n\n```text\n[ERROR] hooks module pbadefault@inline failed to load: pbadefault: options do not fit plugin.json userConfig:\n  Bad must be a number (settings.json pluginConfigs[\"pbadefault\" or \"pbadefault@inline\"].options)\n```\n\nThat error misleads you twice. It names the field by its `title`, so if the title and key differ you have to go looking. And it blames `settings.json` for a value that came from your own manifest's `default`, so you'll go and open a file with nothing in it. Nothing was configured. The default was wrong.\n\nSince 2.1.283 a failed load in a `claude -p` run with `--plugin-dir` also prints a line to plain stderr, `pbadefault: hooks module did not load: ...`, so you'll see it without `--debug`. The same line shape is still there at 2.1.287. That's the only case where stderr gets it. An interactive session that hot-reloads the mod shows it in the transcript, and for a mod installed from a marketplace it's only in the debug log, as Anthropic's troubleshooting page [lays out](https:\/\/code.claude.com\/docs\/en\/plugins\/mods\/troubleshoot#find-out-why-a-mod-does-nothing).\n\n### required: true with nothing configured stops the module loading\n\n```text\nNeeded is required but not provided (settings.json pluginConfigs[...])\n```\n\nThat's the 2.1.286 text too.\n\nIt's not a warning, there's no fallback, and nothing half works. The module doesn't load and none of its hooks exist. So `required: true` means every install of your mod has to be configured before its first run, and when one isn't, your mod just isn't there. A `default` plus a check in your own code is usually the better choice.\n\n### Project settings are never read\n\n```text\nplugin auditone: no pluginConfigs[\"auditone\" or \"auditone@inline\"].options in user, --settings or\nmanaged settings (project settings are not read); every option is its default\n```\n\nClaude Code looks in three places: user settings, `--settings` and managed settings. A repository can't ship configuration for a mod in its project settings. That's a reasonable security call, and it's an easy afternoon to lose if you assume otherwise. The message spells it out, but only in the debug log. It reads the same at 2.1.286.\n\n## Configuring one\n\nConfiguration is keyed by plugin name, and the key includes where it was loaded from:\n\n```json\n{\n  \"pluginConfigs\": {\n    \"my-mod@inline\": {\n      \"options\": { \"endpoint\": \"https:\/\/example.test\/hook\", \"timeoutMs\": 500 }\n    }\n  }\n}\n```\n\n`@inline` is what `--plugin-dir` gives you. The error messages name both keys the loader looks for, which is the easy way to find out what your install is called.\n\n```bash\nclaude --plugin-dir .\/my-mod --settings .\/dev-settings.json\n```\n\nA stored value that isn't one of a field's `options` used to read as unset. At 2.1.286 it reads as the field's default, and the log says so at error level: `popts: option mode in settings is not one of fast, slow; it reads as the default, fast`.\n\nAt 2.1.283 every plugin without a `userConfig` also logged `options requested but its manifest declares no userConfig; every option reads as absent` as a warning. That line isn't in the 2.1.286 binary, and a mod with no `userConfig` that reads `options` just gets `undefined` for each key.\n\n### From the command line\n\nSince 2.1.285 there's `claude plugin configure <plugin>`, which wants the full id, `name@marketplace`. On its own it only reads. This is 2.1.286 on the changelog plugin:\n\n```text\nOptions for cc-changelog (cc-changelog@cc-changelog):\n  \u276f baseUrl \u2014 Site base URL (optional, not set)\n\nSet values with \/plugin configure <plugin> in Claude Code, or pipe a JSON object to `claude plugin configure <plugin> --values-stdin`.\n```\n","previous_page":"ui","next_page":"testing","url":"https:\/\/changelogs.core-directive.com\/reference\/mods\/config","index_url":"https:\/\/changelogs.core-directive.com\/reference\/mods.json"}