From xtralab-skills
Customizes JupyterLab/xtralab appearance, behavior, and extensions. Maps modifications to the correct config surface and writes safe JSON snippets without touching package files.
How this skill is triggered — by the user, by Claude, or both
Slash command
/xtralab-skills:customize-jupyterlabThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
JupyterLab does not have one settings file — it has four layered config surfaces, each with its own format and discovery path. This skill maps a user request to the right surface, the right file, and the right location, then writes a small JSON snippet there. **Read [references/config-paths.md](references/config-paths.md) before editing anything.**
JupyterLab does not have one settings file — it has four layered config surfaces, each with its own format and discovery path. This skill maps a user request to the right surface, the right file, and the right location, then writes a small JSON snippet there. Read references/config-paths.md before editing anything.
Never edit package-installed files. Anything under <venv>/lib/.../site-packages/... or existing directories under <venv>/share/jupyter/... (kernels, shipped labextensions, shipped schemas) is owned by packages and gets overwritten on reinstall. Two exceptions: (a) <venv>/etc/jupyter/ is a config dir Jupyter searches, and you may write user snippets there (see rule 2); (b) adding a new sub-directory under <venv>/share/jupyter/labextensions/<your-name>/ is the intended install location for a federated labextension (see references/adhoc-extensions.md).
Pick the right config dir, by priority. Run jupyter --paths and look at the config section. Treat that list as authoritative, then write to the first non-system writable config dir whose scope matches the request:
etc/jupyter/ (<venv>/etc/jupyter/, $CONDA_PREFIX/etc/jupyter/, or whatever sys.prefix/etc/jupyter resolves to) — preferred when a normal project venv or conda env is active. Scopes the customization to this project/env, travels with it, and is what users typically want when they're working inside a venv. Caveat: rebuilding the env (rm -rf .venv && uv sync, conda env remove, pip install --force-reinstall jupyterlab) wipes it. Tell the user this when you write here.jupyter --paths — usually ~/.jupyter/, but JUPYTER_CONFIG_DIR replaces this slot when it is set (xtralab desktop app, JupyterHub spawners, some Docker images). Use this when no project env is active, when the user wants the change to apply broadly, or when the active env path is a bundled/installed runtime rather than the user's project env./usr/local/etc/jupyter/ or /etc/jupyter/ without explicit user/admin intent — those are system-wide.If you are unsure which of (1) and (2) the user wants, default to the env path when a normal project env is active and mention the trade-off in your reply. For the xtralab desktop app, write to its JUPYTER_CONFIG_DIR path, not the bundled runtime's etc/jupyter/.
One concern per snippet file. Prefer creating a new file like 99-user-theme.json next to existing files rather than editing a shipped 00-xtralab.json or 00-ajlab.json. JupyterLab merges all *.json files in a *.d/ directory; lexical order breaks ties, so 99- wins.
Validate JSON before saving. A malformed file silently disables every snippet in the directory.
Restart JupyterLab after writing. The browser-side Settings → Settings Editor UI shows user-settings live; page_config.d/ and default_setting_overrides.d/ need a server restart. For the xtralab desktop app: quit and relaunch.
| Surface | Path pattern (under a config dir) | Use for |
|---|---|---|
| Page config | labconfig/page_config.d/*.json | Disable or re-enable extensions, set page-level flags (theme name when no user setting, devmode, etc.) |
| Default setting overrides | labconfig/default_setting_overrides.d/*.json | Change the default value of any JupyterLab plugin setting (theme, shell layout, codemirror, completer, git, terminal, …). User settings still win over this. |
| Jupyter server config | jupyter_server_config.d/*.json (plus the larger jupyter_server_config.py) | Enable a server extension, register a language server for jupyterlab-lsp, set tornado/server traits |
| User settings | lab/user-settings/<plugin-id>/<setting>.jupyterlab-settings | Per-user override of one specific setting; highest priority. Written by the in-app Settings Editor but you can drop files here too. |
A user request usually maps to one of these. If you are not sure, default to setting overrides for visible JupyterLab behavior and page config for hiding extensions entirely. See references/recipes.md for the mapping per request type.
The four config surfaces above can only reshape what JupyterLab already exposes. To add new behavior — a toolbar button, a status-bar item, a sidebar panel, a custom command, a launcher card, a file handler, a mime renderer — you have to ship a labextension.
This is a fifth surface, and it does not need a full TypeScript / pip-package setup. A labextension is a directory under <prefix>/share/jupyter/labextensions/<name>/ containing a package.json and a JS file that registers a webpack-module-federation container on window._JUPYTERLAB[<name>]. JupyterLab discovers it at server startup and loads it like any other federated extension. For the common case (consuming only packages JupyterLab already ships — anything in @jupyterlab/* or @lumino/*), you can hand-write the container in plain JS — no build, no node_modules.
Decide config vs. extension with the table in references/adhoc-extensions.md. Anything phrased as "add", "new", "custom", or describing behavior that does not exist yet is an extension request. Read that reference before scaffolding — it has the full federation contract, the plugin object shape, ready-to-use patterns (toolbar button, command + palette + shortcut, status-bar item, sidebar, launcher, file type, mime renderer), and the gotchas (sync factory requirement, shared-scope token identity, etc.).
For every customization request:
jupyter --paths. Prefer the active project env's etc/jupyter/ when it is a normal venv/conda env; otherwise use the user-config entry shown by jupyter --paths (JUPYTER_CONFIG_DIR when set, ~/.jupyter/ otherwise). When you write to an env path, tell the user the customization is scoped to that env and will be lost if the env is rebuilt.99-*.json instead.Help → JupyterLab → About showing the disabled extensions or with Settings → Settings Editor.If the user is on xtralab (not vanilla JupyterLab), some requests have a dedicated setting that is friendlier than disabling plugins. The full settings UI is at Settings → Settings Editor → xtralab …:
xtralab:sidebar settings — showDefaultFileBrowser, showRunningSessions. Prefer this over page_config.disabledExtensions because it preserves discoverability via the View menu.xtralab:launcher — agents[] and editors[] arrays merge with built-ins by id. See xtralab's README for the schema; set enabled: false to hide a default agent, or add a new entry to introduce one.schema/plugin.json. The shipped menu is opinionated; users override entries by writing user-settings for xtralab:plugin.Write xtralab-specific overrides to the same user config dir, as default_setting_overrides.d/99-xtralab-user.json or as a lab/user-settings/xtralab/<name>.jupyterlab-settings file.
jupyter --paths shows no writable user dir, or if the user appears to be in a Docker/JupyterHub setup where the right path is image-specific.These are JupyterLab-specific traps that an agent will fall into without warning:
page_config.d/ and default_setting_overrides.d/ are read at server start, not at page reload. A browser refresh will not pick up the change. The user has to restart jupyter lab (or quit and relaunch the xtralab desktop app).~/.jupyter/. It sets JUPYTER_CONFIG_DIR to a per-app directory (~/Library/Application Support/xtralab/jupyter/config/ on macOS, ~/.config/xtralab/jupyter/config/ on Linux). Writing to ~/.jupyter/ has no effect on the desktop app — always confirm with jupyter --paths from inside the running environment.:. Plugin ID xtralab:launcher lives at lab/user-settings/xtralab/launcher.jupyterlab-settings, not lab/user-settings/xtralab:launcher.jupyterlab-settings. The directory is the part before the colon; the filename is the part after, with extension .jupyterlab-settings.disabledExtensions: { "<id>": false } re-enables a plugin that a lower-priority file disabled. Omitting the entry is not the same as setting it to false — to revert xtralab's 00-xtralab.json disabling the debugger, write a 99-user.json with false explicitly.lab/user-settings/...jupyterlab-settings file, then default_setting_overrides.d/*.json (lexically last wins), then the plugin's schema default. And page_config.d/'s disabledExtensions short-circuits all of this — a disabled plugin ignores every setting. If a change "doesn't apply", check each layer in that order.jupyter.lab.menus and jupyter.lab.shortcuts overrides merge with shipped items, they don't replace them. To remove a menu entry, use { "id": "...", "disabled": true }, not omission.$PATH for the Jupyter Server process. Registering a server in jupyter_server_config.d/ only tells jupyterlab-lsp how to spawn it; the binary itself must already be installed and resolvable from the Jupyter server's environment (not just the user's shell — these differ for the desktop app and for some launcher setups).jupyter labextension list shows package names, not plugin IDs. A package like @jupyterlab/notebook-extension contributes multiple plugins (:tracker, :tools, :completer, …). To find the exact ID a disabledExtensions key needs, open Help → JupyterLab → About in the running app, or grep the extension's schemas/*.json files in share/jupyter/lab/schemas/.lab/user-settings/, not in default_setting_overrides.d/. Changes the user makes through the UI are per-user; they do not propagate to other users on the same machine and they win over any default you set in default_setting_overrides.d/.After every change, tell the user to:
Ctrl-C and rerun jupyter lab) or quit + relaunch the desktop app.Help → JupyterLab → About to confirm the expected plugin appears disabled / enabled.Settings → Settings Editor → <plugin name> — defaults set via default_setting_overrides.d/ show with a tooltip noting "system default".Settings → LSP shows the server as connected.If the change does not appear, walk the three priority layers before re-editing the file.
# Where do configs live?
jupyter --paths
# Which lab extensions are installed (and disabled)?
jupyter labextension list
# Which server extensions are enabled?
jupyter server extension list
# Which language servers does jupyterlab-lsp know about?
# (look at the LanguageServerManager.language_servers section)
jupyter server --debug 2>&1 | head -200
JUPYTER_CONFIG_DIR overrides.page_config.json, overrides.json): https://jupyterlab.readthedocs.io/en/latest/user/directories.html#labconfig-directoriesdisabledExtensions): https://jupyterlab.readthedocs.io/en/latest/user/extensions.html.d/ directories): https://jupyter-server.readthedocs.io/en/latest/operators/configuring-extensions.htmlCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub jtpio/xtralab --plugin xtralab-skills