This guide covers deploying Notebook Intelligence at scale — JupyterHub, KubeSpawner, Kubeflow, multi-tenant clusters, regulated environments. For end-user documentation, see the README.
NBI is a per-user tool. Every section below assumes the extension runs inside a per-user Jupyter Server, not a shared one. Server-side state is per-user; there is no central NBI service.
- Install layout and config precedence
- Persistent-volume layout
- Shared filesystem and multi-user notes
- Environment variables and traitlets
- Security model
- API-key handling
- Self-hosted LLM endpoints
- Custom CA certs and corporate proxies
- Air-gap deployment
- HIPAA / sensitive-data preset
- Restricting features for managed deployments
- Multi-tenancy and per-team scoping
- Managed Claude Skills token
- Chat feedback event hook
- HTTP API surface
- Failure modes
- Version matrix
- FIPS posture
- Resource footprint
NBI reads configuration from three layers, listed in order of precedence (later wins):
- Environment-wide base config —
<env-prefix>/share/jupyter/nbi/config.jsonand<env-prefix>/share/jupyter/nbi/mcp.json. Bake into your image. Read once at startup. - User config —
~/.jupyter/nbi/config.jsonand~/.jupyter/nbi/mcp.json. The user mutates these via the Settings dialog. Lives on the per-user PVC. - Environment variables —
NBI_*and certain provider variables (see the reference table). Override at pod startup time.
Traitlets configured via JupyterLab CLI flags or jupyter_server_config.py (e.g., c.NotebookIntelligence.disabled_providers = [...]) are evaluated at server startup. Most env-var overrides (NBI_*_POLICY, NBI_ALLOW_GITHUB_*, NBI_*_MANAGEMENT_POLICY, etc.) are also resolved once at startup and cached on the handler classes — flipping them requires a JupyterLab restart. The NBI_ENABLED_PROVIDERS and NBI_ENABLED_BUILTIN_TOOLS re-enable env vars (gated by allow_enabling_*_with_env) are the exception: those are read on every request.
Manual edits to config.json while JupyterLab is running require a JupyterLab restart to take effect. Edits via the Settings dialog are picked up live.
| Path | Persist? | Notes |
|---|---|---|
~/.jupyter/nbi/config.json |
Yes | User's chosen provider, models, MCP servers, plus plaintext API keys. Treat as a secret. |
~/.jupyter/nbi/user-data.json |
Yes | Encrypted GitHub Copilot access token, written when "remember login" is enabled. Encrypted with NBI_GH_ACCESS_TOKEN_PASSWORD. |
~/.jupyter/nbi/rules/ |
Yes | User's ruleset markdown files. |
~/.jupyter/nbi/mcp.json |
Yes | User's MCP server config (alternative to managing via the Settings dialog). |
~/.claude/skills/ |
Yes | User-scope Claude skills (including managed skills). |
~/.claude/projects/ |
Yes | Claude Code session transcripts. Required for "Resume previous Claude session". Managed by Claude CLI, not NBI. |
<env-prefix>/share/jupyter/nbi/ |
No (image) | Org-wide base config. Bake into your container image. |
Project-scope <project>/.claude/skills/ |
Per project | Lives in the user's working directory. Persists if the working directory does. |
For Kubeflow or KubeSpawner: mount the user's home directory on a PVC and ensure ~/.jupyter and ~/.claude are inside that mount. Anything else (/tmp, ~/.cache) can be ephemeral.
If users share a home directory across nodes (NFS-backed shared HPC, classroom labs):
- Race conditions in
~/.jupyter/nbi/. Concurrent writes from two login nodes can corruptconfig.json. NBI does not file-lock. Pin each user to one node, or use a per-node config prefix. NBI_GH_ACCESS_TOKEN_PASSWORDdefault is unsafe. The default password (nbi-access-token-password) is shared across installs. On a multi-tenant cluster, anyone with read access to another user's~/.jupyter/nbi/user-data.jsoncan decrypt their Copilot token. Set a per-user password (e.g., derived from the Hub user secret), or disable "remember login" entirely (see Restricting features).- Skill collisions. Two users sharing
~/.claude/skills/will see each other's skills. Make sure each user has a unique home.
The full surface, in one table.
| Name | Type | Default | Source | Purpose |
|---|---|---|---|---|
disabled_providers |
List | [] |
traitlet on NotebookIntelligence |
Hide providers from the user dropdown. Values: github-copilot, ollama, litellm-compatible, openai-compatible. |
allow_enabling_providers_with_env |
Bool | False |
traitlet | If true, NBI_ENABLED_PROVIDERS re-enables hidden providers per pod. |
NBI_ENABLED_PROVIDERS |
csv | unset | env | Comma-separated provider IDs to re-enable. Effective only when allow_enabling_providers_with_env=True. |
disabled_tools |
List | [] |
traitlet | Hide built-in tools from agent mode. Values listed in Restricting features. |
allow_enabling_tools_with_env |
Bool | False |
traitlet | If true, NBI_ENABLED_BUILTIN_TOOLS re-enables hidden tools per pod. |
NBI_ENABLED_BUILTIN_TOOLS |
csv | unset | env | Comma-separated tool IDs to re-enable. Effective only when allow_enabling_tools_with_env=True. |
disabled_coding_agent_launchers |
List | [] |
traitlet | Hide JupyterLab launcher tiles for coding-agent CLIs even when the CLI is on PATH. Valid IDs: claude-code, opencode, pi, github-copilot-cli, codex. See Disabling coding-agent launcher tiles. |
allow_enabling_coding_agent_launchers_with_env |
Bool | False |
traitlet | If true, NBI_ENABLED_CODING_AGENT_LAUNCHERS re-enables hidden tiles per pod. |
NBI_ENABLED_CODING_AGENT_LAUNCHERS |
csv | unset | env | Comma-separated launcher IDs to re-enable. Effective only when allow_enabling_coding_agent_launchers_with_env=True. |
enable_chat_feedback |
Bool | False |
traitlet | Enables thumbs-up/down UI in chat and emits in-process telemetry events. |
additional_skipped_workspace_directories |
List | [] |
traitlet | Extra directory names to skip in the chat-sidebar @-mention workspace file picker. Merged with the built-in skips (__pycache__, node_modules). Match is by directory name only, case-sensitive. |
NBI_ADDITIONAL_SKIPPED_WORKSPACE_DIRECTORIES |
csv | unset | env (appends to traitlet) | Comma-separated extra directory names. Resolved at server startup and concatenated with the traitlet value, so a spawn profile can add to (rather than replace) the org-wide list. |
allow_github_skill_import |
Bool | True |
traitlet | When False, hides the Import from GitHub button in the Skills panel and rejects /skills/import POSTs with 403. Does not affect the managed-skills reconciler. |
NBI_ALLOW_GITHUB_SKILL_IMPORT |
bool | unset | env (overrides traitlet) | Per-pod override for allow_github_skill_import. Accepts true/false/1/0/yes/no/on/off (case-insensitive). Useful for varying the policy across spawn profiles. |
skills_manifest |
str | "" |
traitlet | URL or filesystem path to a managed-skills manifest, or a comma-separated list of either. Manifests are unioned with first-wins URL dedupe; name collisions surface as per-entry errors. See docs/skills.md. |
NBI_SKILLS_MANIFEST |
str | unset | env (overrides traitlet) | Same as above; env takes precedence. |
skills_manifest_interval |
int | 86400 |
traitlet | Seconds between reconciles. |
NBI_SKILLS_MANIFEST_INTERVAL |
int | unset | env (overrides traitlet) | Same as above; env takes precedence. |
managed_skills_token |
str | "" |
traitlet | Bearer token for managed-skills GitHub fetches. |
NBI_MANAGED_SKILLS_TOKEN |
str | unset | env (overrides traitlet) | Same as above; env takes precedence. |
allow_github_plugin_import |
Bool | True |
traitlet | When False, hides the "From GitHub" affordance in the Plugins panel and rejects claude plugin marketplace add requests whose source resolves as a GitHub URL or owner/repo shorthand. Local-path and arbitrary-URL sources remain available. |
NBI_ALLOW_GITHUB_PLUGIN_IMPORT |
bool | unset | env (overrides traitlet) | Per-pod override for allow_github_plugin_import. Accepts true/false/1/0/yes/no/on/off (case-insensitive). |
skill_max_archive_mb |
Int | 100 |
traitlet | Per-archive on-wire size cap (megabytes) for skill bundles fetched from GitHub. Applies to both user imports and managed-skills tarballs. 0 disables the cap. |
NBI_SKILL_MAX_ARCHIVE_MB |
int | unset | env (overrides traitlet) | Same as above; env takes precedence. |
upload_max_mb |
Int | 50 |
traitlet | Per-file size cap (megabytes) for the shared upload endpoint used by chat-sidebar attachments and terminal drag-drop. Requests over the cap return HTTP 413. 0 disables the cap. |
NBI_UPLOAD_MAX_MB |
int | unset | env (overrides traitlet) | Same as above; env takes precedence. |
upload_retention_hours |
Int | 24 |
traitlet | How long staged uploads survive in the temp directory before the next upload sweeps them. 0 keeps only the atexit purge (uploads survive the session). |
NBI_UPLOAD_RETENTION_HOURS |
int | unset | env (overrides traitlet) | Same as above; env takes precedence. |
tour_config_path |
str | "" |
traitlet | Filesystem path to a YAML/JSON file with admin overrides for the first-run sidebar tour copy. See docs/admin-tour-config.md. |
NBI_TOUR_CONFIG_PATH |
str | unset | env (overrides traitlet) | Same as above; env takes precedence. |
NBI_GH_ACCESS_TOKEN_PASSWORD |
str | nbi-access-token-password |
env | Password used to encrypt the stored Copilot token in user-data.json. Change in multi-tenant deployments. |
NBI_RULES_AUTO_RELOAD |
bool | true |
env | When false, ruleset edits require a JupyterLab restart to take effect. |
NBI_CLAUDE_CLI_PATH |
str | unset | env | Absolute path to the Claude Code CLI binary. When unset, NBI looks up claude on PATH. |
NBI_OPENCODE_CLI_PATH |
str | unset | env | Absolute path to the opencode CLI. When unset, NBI looks up opencode on PATH. Gates the opencode launcher tile. |
NBI_PI_CLI_PATH |
str | unset | env | Absolute path to the Pi CLI. When unset, NBI looks up pi on PATH. Gates the Pi launcher tile. |
NBI_GITHUB_COPILOT_CLI_PATH |
str | unset | env | Absolute path to the GitHub Copilot CLI. When unset, NBI looks up copilot on PATH. Gates the GitHub Copilot launcher tile. |
NBI_CODEX_CLI_PATH |
str | unset | env | Absolute path to the OpenAI Codex CLI. When unset, NBI looks up codex on PATH. Gates the Codex launcher tile. |
NBI_GHE_SUBDOMAIN |
str | "" |
env | GitHub Enterprise subdomain for GitHub Copilot users on a GHE tenant. Empty selects github.com. |
NBI_GITHUB_ENTERPRISE_HOSTS |
csv | "" |
env | Comma-separated hostnames the plugin marketplace detector treats as GitHub. Cookie-domain shape: bare token (github.acme.com) matches exactly; leading-dot token (.acme.com) matches any subdomain of acme.com. Independent of NBI_GHE_SUBDOMAIN, which only configures the Copilot OAuth tenant. Required so allow_github_plugin_import = False actually gates GHE marketplace adds and so the GITHUB_TOKEN / gh auth token chain injects on GHE sources. |
NBI_LOG_LEVEL |
str | INFO |
env | Python logging level for the notebook_intelligence logger. |
NBI_DISABLE_OUTPUT_SCRUB |
bool | unset | env | When set (1 / true / yes / on), disables the shell-tool output scrubber so raw stdout/stderr (including any env-var values that leak) is sent through to chat. Default off; the scrubber redacts values for sensitive-named env vars (TOKEN, SECRET, API_KEY, ...) plus tokens with well-known credential prefixes (ghp_, sk-ant-, AKIA, ...). Opt out only when debugging credential helpers where the redaction interferes. |
GITHUB_TOKEN, GH_TOKEN |
str | unset | env | Used (in that order) by user-initiated skill imports and GitHub-sourced plugin marketplace adds for GitHub auth. Falls back to gh CLI auth. |
NBI_*_POLICY |
str | user-choice |
env | Lock individual Settings panel toggles. See README → Admin policies for the full list of *_POLICY env vars and matching traitlets, including NBI_SKILLS_MANAGEMENT_POLICY, NBI_CLAUDE_MCP_MANAGEMENT_POLICY, NBI_CLAUDE_PLUGINS_MANAGEMENT_POLICY, NBI_TERMINAL_DRAG_DROP_POLICY, and NBI_REFRESH_OPEN_FILES_ON_DISK_CHANGE_POLICY. |
Configure traitlets in jupyter_server_config.py:
c.NotebookIntelligence.disabled_providers = ["openai-compatible", "litellm-compatible"]
c.NotebookIntelligence.allow_enabling_providers_with_env = True
c.NotebookIntelligence.disabled_tools = ["nbi-command-execute"]
c.NotebookIntelligence.skills_manifest = "https://internal.example.com/manifests/data-science-team.yaml"NBI runs entirely inside the user's Jupyter Server process. There is no privilege boundary between NBI and the user. In particular:
- Built-in tools execute as the user.
nbi-command-executeruns arbitrary shell commands.nbi-file-editandnbi-file-readread and write any file the user can.nbi-notebook-editandnbi-notebook-executemodify and run notebooks.
- MCP stdio servers are launched as user subprocesses with the user's environment. NBI does not sandbox them.
- Claude Code CLI inherits the user's environment, including filesystem permissions and any auth tokens in
~/.claude/.
For regulated tenants:
- Disable the most powerful tools — at minimum
nbi-command-executeandnbi-file-edit. See Restricting features. - Restrict the providers the user can pick. Force a single self-hosted endpoint with
disabled_providersplus the org's base config. - Disable user-initiated skill imports with
allow_github_skill_import = False(envNBI_ALLOW_GITHUB_SKILL_IMPORT=false), and plugin marketplace adds from GitHub withallow_github_plugin_import = False(envNBI_ALLOW_GITHUB_PLUGIN_IMPORT=false). Reinforce at the network layer where stronger isolation is required. Seeskills.mdand the Plugins tab section below. - Run with a non-root container user, with no host-network access and no host-path mounts beyond the user's PVC.
By default, custom-provider API keys (Anthropic, OpenAI-compatible, LiteLLM-compatible) are stored plaintext in ~/.jupyter/nbi/config.json. This is acceptable for single-tenant developer workstations and unacceptable for multi-tenant clusters.
Recommended approach for clusters:
- Inject the org's keys via env vars at pod startup. Set the provider's expected env var (
OPENAI_API_KEY,ANTHROPIC_API_KEY, etc.) on the pod. Configure the provider in<env-prefix>/share/jupyter/nbi/config.jsonwithout a key — NBI picks up the provider's standard env var. - Source secrets from your secret manager. Vault, External Secrets Operator, AWS Secrets Manager, GCP Secret Manager, or KubeSpawner's
c.KubeSpawner.environmentcallback can all populate the pod env from a secret backend at spawn time. - Don't commit
config.json. Even the env-prefix base config should not contain keys; pull keys from env at spawn.
${ENV_VAR}-style interpolation inside config.json is not currently supported. Tracked as a feature request.
NBI's openai-compatible and litellm-compatible providers can target any endpoint that speaks the respective wire format.
Azure OpenAI (via the openai-compatible provider):
{
"providers": {
"openai-compatible": {
"base_url": "https://my-resource.openai.azure.com/openai/deployments/gpt-4-deployment",
"api_key": "${AZURE_OPENAI_KEY}",
"default_chat_model": "gpt-4",
"default_inline_completion_model": "gpt-4"
}
}
}vLLM, TGI, or any local OpenAI-compatible server:
{
"providers": {
"openai-compatible": {
"base_url": "http://internal-vllm.example.com:8000/v1",
"api_key": "any-string-the-server-accepts",
"default_chat_model": "meta-llama/Meta-Llama-3-70B-Instruct"
}
}
}LiteLLM proxy (so you can route to many upstream models from one place, including Bedrock, Vertex, etc.):
{
"providers": {
"litellm-compatible": {
"base_url": "https://litellm.internal.example.com",
"api_key": "${LITELLM_TOKEN}"
}
}
}Bake the base config into your image and let users select their model from the dropdown.
NBI's HTTP requests use Python's requests and httpx, plus the litellm, openai, and anthropic SDKs. All honor standard Python TLS and proxy environment variables:
REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt
SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
HTTPS_PROXY=http://corp-proxy.example.com:3128
HTTP_PROXY=http://corp-proxy.example.com:3128
NO_PROXY=localhost,127.0.0.1,.cluster.localSet these on the pod (c.KubeSpawner.environment for Hub; environment in your Dockerfile or compose file otherwise). The Claude Code CLI is a separate Node.js process and reads the same HTTPS_PROXY and NODE_EXTRA_CA_CERTS conventions.
The frontend talks only to its own Jupyter Server backend, which proxies the LLM calls. The browser does not need a corporate CA trust.
Steps for deploying to a network with no general internet egress:
-
Pre-build the Docker image with NBI installed, the Claude Code CLI binary baked in, and any MCP server packages pre-installed (do not rely on
npx -yat runtime). -
Manifest hosting. Set
NBI_SKILLS_MANIFESTto either afile://path (a manifest baked into the image) or an internalhttps://URL on a network the pod can reach. -
Skill bundles. Either bake the skills into the image under
~/.claude/skills/(managed status will reset since they aren't from the manifest), or host the GitHub-style tarballs at an internal mirror and write the manifest URLs to point at it. -
Disable user-initiated GitHub imports at the network layer — block
github.com,codeload.github.com, andraw.githubusercontent.com. Users can still install skills from the local filesystem by dropping bundles into~/.claude/skills/. -
MCP
npx -yis incompatible with air-gap. Pre-install the server binary and reference it directly:{ "mcpServers": { "filesystem": { "command": "/opt/mcp/bin/mcp-server-filesystem", "args": ["/home/user/work"] } } } -
LLM endpoint. Air-gap requires a self-hosted endpoint (vLLM, TGI, or a LiteLLM proxy in front of a VPC-endpoint Bedrock, etc.). See Self-hosted LLM endpoints.
For deployments that must not transmit PHI to cloud LLM providers, force local-only models:
# jupyter_server_config.py
c.NotebookIntelligence.disabled_providers = [
"github-copilot",
"openai-compatible",
"litellm-compatible",
]
c.NotebookIntelligence.allow_enabling_providers_with_env = False # users cannot override
c.NotebookIntelligence.disabled_tools = ["nbi-command-execute", "nbi-file-edit"]
c.NotebookIntelligence.allow_enabling_tools_with_env = FalsePair with <env-prefix>/share/jupyter/nbi/config.json shipping an Ollama provider preconfigured against your local model:
{
"default_provider": "ollama",
"providers": {
"ollama": {
"base_url": "http://ollama-internal.example.com:11434",
"default_chat_model": "llama3:70b",
"default_inline_completion_model": "codellama:7b"
}
}
}Block egress to all external LLM hosts at the network layer as defense in depth (see PRIVACY.md for the full list).
This is a starting point, not a HIPAA compliance certification. Run a security review of the full stack (Ollama, your Jupyter image, KubeSpawner, network policy) before treating any data as protected.
NBI's denylist for providers and tools follows the same shape:
c.NotebookIntelligence.disabled_providers = ["ollama", "litellm-compatible", "openai-compatible"]Valid IDs: github-copilot, ollama, litellm-compatible, openai-compatible.
To allow per-pod re-enable via env var:
c.NotebookIntelligence.allow_enabling_providers_with_env = TrueNBI_ENABLED_PROVIDERS=github-copilot,ollamac.NotebookIntelligence.disabled_tools = ["nbi-notebook-execute", "nbi-python-file-edit"]Valid IDs: nbi-notebook-edit, nbi-notebook-execute, nbi-python-file-edit, nbi-file-edit, nbi-file-read, nbi-command-execute.
To allow per-pod re-enable:
c.NotebookIntelligence.allow_enabling_tools_with_env = TrueNBI_ENABLED_BUILTIN_TOOLS=nbi-notebook-execute,nbi-python-file-editNBI does not currently support an explicit allowlist mode (allowed_providers, allowed_tools). A new built-in provider added in a minor release would auto-enable for users with disabled_providers=[]. If this matters for your compliance posture, pin to specific NBI versions and review changelog entries before upgrading. Tracked as a feature request.
The JupyterLab launcher shows a tile for each coding-agent CLI on PATH: Claude Code, opencode, Pi, GitHub Copilot CLI, and Codex. Tile visibility is gated by CLI presence; to hide a tile even when the CLI is present (for example, to keep users in the chat sidebar's audit path), add its ID to the denylist:
c.NotebookIntelligence.disabled_coding_agent_launchers = ["opencode", "pi", "codex"]Valid IDs: claude-code, opencode, pi, github-copilot-cli, codex. Unknown IDs raise at server startup so a typo can't silently no-op the policy.
The github-copilot-cli ID is deliberately distinct from github-copilot (the disabled_providers value for the Copilot LLM provider). The tile and the provider are independent surfaces; hiding the tile does not affect chat with the Copilot provider, and vice versa.
To vary the policy per spawn profile, opt into per-pod re-enable:
c.NotebookIntelligence.allow_enabling_coding_agent_launchers_with_env = TrueNBI_ENABLED_CODING_AGENT_LAUNCHERS=claude-code,codexThe env var has effect only when allow_enabling_coding_agent_launchers_with_env = True; without that flag, the denylist is final. The merged effective set is computed per request and the frontend re-evaluates tile visibility on each capabilities refresh, so an env-var flip applies after a page reload in the same session. Edits to the disabled_coding_agent_launchers traitlet in jupyter_server_config.py require a JupyterLab server restart, the same as other traitlet edits.
Blast radius. The denylist hides the launcher tile and removes the matching JupyterLab command-palette entry. The CLI binary remains on
PATHand remains usable from a manually-opened terminal. To prevent terminal use, restrict the binary at the container-image orPATHlevel. To prevent the Claude chat mode itself (a separate surface from the launcher tile), useNBI_CLAUDE_MODE_POLICY=force-offfrom the Admin policies table. Note thatNBI_CLAUDE_MODE_POLICY=force-offdoes not imply hiding the Claude Code launcher tile: the tile runsclaudedirectly in a terminal and is independent of the chat-mode SDK backend. To hide both surfaces, combineNBI_CLAUDE_MODE_POLICY=force-offwithdisabled_coding_agent_launchers = ["claude-code"].
Per-pod re-enable trust model. Setting
allow_enabling_coding_agent_launchers_with_env = Truedelegates the final denylist decision to whatever process setsNBI_ENABLED_CODING_AGENT_LAUNCHERSon the pod. If your spawn-profile config is itself user-influenced (profile-form fields, untrusted YAML), keep this flagFalseso the traitlet baseline is authoritative.
c.NotebookIntelligence.allow_github_skill_import = FalseHides the Import from GitHub button in the Skills panel and rejects POSTs to /notebook-intelligence/skills/import and /notebook-intelligence/skills/import/preview with HTTP 403. This does not disable the managed-skills reconciler; admin-curated skills delivered via NBI_SKILLS_MANIFEST continue to install. Use this when you want to allow only org-vetted skills.
To vary the policy per spawn profile, override at pod startup:
NBI_ALLOW_GITHUB_SKILL_IMPORT=falseThe env var wins over the traitlet and is resolved at server startup. Recognized values: true/false/1/0/yes/no/on/off, case-insensitive. Unrecognized values raise at startup so a typo can't silently flip the policy.
Audience: server admins (and end users who edit ~/.jupyter/nbi/config.json directly). The setting is not exposed in the Settings dialog.
The @-mention picker in the chat sidebar enumerates files from the JupyterLab working directory and skips a built-in set of directories (__pycache__, node_modules) plus any dotfiles/dot-directories. Because dot-prefixed names are filtered separately, entries starting with . are no-ops; list non-dot names only.
Match is by directory name only (not path), case-sensitive. Use this when a project has standard build outputs the picker shouldn't surface.
Four layers can contribute, all additive (each layer can only add new skipped names, never re-expose names skipped by an earlier layer):
-
Traitlet in
jupyter_server_config.py:c.NotebookIntelligence.additional_skipped_workspace_directories = ["build", "dist", "target"]
-
Env var at pod startup (per spawn profile):
NBI_ADDITIONAL_SKIPPED_WORKSPACE_DIRECTORIES=tmp,artifacts
-
Env-prefix NBI config at
<env-prefix>/share/jupyter/nbi/config.json(org-wide, baked into the image — useful when the deployment doesn't managejupyter_server_config.pybut does ship a curated config file):{ "additional_skipped_workspace_directories": ["coverage", "out"] } -
User NBI config at
~/.jupyter/nbi/config.json(per-user extension on top of the admin baseline):{ "additional_skipped_workspace_directories": [".terraform"] }
Duplicates are collapsed; the merged list is then added to the built-in skip set on the frontend. Edits to config.json require a JupyterLab restart to take effect, matching the rest of NBI config — there's no UI control (issue #232).
c.NotebookIntelligence.skills_management_policy = "force-off"Or via env: NBI_SKILLS_MANAGEMENT_POLICY=force-off.
Force-off does three things at once:
- Hides the Skills tab in the Settings panel.
- Returns HTTP 403 from every
/notebook-intelligence/skills/*route, so a stale frontend or a direct API caller can't read or write skills. - Suppresses the managed-skills reconciler — the manifest is treated as empty, no
SkillReconcileris constructed, and no scheduled reconcile runs. Org-curated skills still on disk are not touched, but new manifests aren't pulled. Takes effect on JupyterLab server restart. For incident-response without a restart, see the kill switch below.
If the manifest URL or the managed-skills token is compromised and you need to stop the in-flight reconciler immediately, two non-restart paths are now available:
-
HTTP kill switch:
POST /notebook-intelligence/skills/reconciler/stop. Authenticated (Jupyter session token). Stops the background reconciler and returns{"stopped": true, "was_running": <bool>}. Idempotent; safe to script across pods.curl -X POST -H "Authorization: token $JUPYTER_TOKEN" \ https://hub.example.com/user/<name>/notebook-intelligence/skills/reconciler/stop
-
Env-var flip: the reconciler re-reads
NBI_SKILLS_MANAGEMENT_POLICYat the start of each cycle and self-stops if it readsforce-off. If your platform supports in-place pod env updates (rare), this fires the kill switch on the next reconcile boundary (default 24h). For most deployments the HTTP route is the faster option.
Either mechanism only stops the background thread; existing skill bundles on disk remain. Use claude or filesystem tooling to remove them if needed.
No live restart. Once stopped, the reconciler stays stopped for the life of the JupyterLab process. There is no
/startcompanion endpoint; bouncing the server is the only path to re-enable reconciliation in the same pod. This is intentional: a kill switch that another script can flip back on isn't a kill switch.Per-user trust. The endpoint is authenticated with the user's Jupyter session token, not an admin claim. In hub deployments where the JupyterLab pod owner is not the policy admin (typical for JupyterHub), a tenant can stop their own pod's reconciler. The reconciler is per-pod, so this only affects that user's managed-skills delivery. For deployments that need to prevent self-stop, leave
skills_management_policyatuser-choiceand rely on the manifest-fetch token's scope as the auth boundary instead.
Use this section's full-disable when an org wants to disable user-authored Claude skills entirely.
Blast radius. Force-off only kills the management UI — skill bundles already on disk under
~/.claude/skills/or a project's.claude/skills/keep being discovered by Claude Code itself because Claude's skill loader doesn't consult NBI's policy. To stop existing skills from loading, remove them on disk before flipping the policy.
c.NotebookIntelligence.claude_mcp_management_policy = "force-off"Or via env: NBI_CLAUDE_MCP_MANAGEMENT_POLICY=force-off.
Force-off:
- Hides the Claude-mode MCP Servers tab in the Settings panel (visible only when Claude mode is on and the
claudeCLI is available). - Returns HTTP 403 from every
/notebook-intelligence/claude-mcp/*route.
The Claude-mode tab is independent of the existing non-Claude MCP Servers tab. The former wraps Claude Code's own config (~/.claude.json and project .mcp.json); the latter manages NBI's own MCP servers used by the non-Claude chat path. They never appear at the same time — the non-Claude tab is hidden when Claude mode is on, and the Claude-mode tab is hidden when it's off.
Reads come from Claude's JSON config files directly (fast, no health checks). Writes (add / remove) shell out to claude mcp add / claude mcp remove so Claude remains the source of truth for any side effects (project-trust prompts, OAuth bookkeeping).
Blast radius. Force-off only kills the management UI — MCP servers already configured in
~/.claude.jsonor<cwd>/.mcp.jsonkeep loading inside Claude Code sessions because Claude's MCP loader doesn't consult NBI's policy. To stop existing servers, remove them on disk (or via theclaude mcp removeCLI) before flipping the policy.
Trust model. MCP servers run as subprocesses (stdio transport) or accept arbitrary URLs (sse/http transport) inside Claude Code sessions; NBI does not validate or sandbox the command, environment, or network endpoint beyond rejecting CLI flag-smuggling. For multi-tenant or regulated deployments, default to
claude_mcp_management_policy = force-offand ship a curated set of servers via~/.claude/settings.jsoninstead.
c.NotebookIntelligence.claude_plugins_management_policy = "force-off"Or via env: NBI_CLAUDE_PLUGINS_MANAGEMENT_POLICY=force-off.
Force-off hides the Plugins tab and returns 403 from every /notebook-intelligence/plugins/* route. The tab is otherwise visible only when Claude mode is on and the claude CLI is available. Both reads (claude plugin list --json) and writes (claude plugin install / uninstall / enable / disable / marketplace add / marketplace remove) shell out to the Claude CLI; Claude owns the plugin state under ~/.claude/plugins/.
Blast radius. Force-off only kills the management UI — already-installed plugins keep loading inside Claude Code sessions because Claude's plugin loader doesn't consult NBI's policy. To stop existing plugins from loading, you'd need to remove them on disk or disable them via the
claude plugin disableCLI before flipping the policy. Force-off prevents user-driven add/remove/enable/disable through NBI; that's the contract.
To allow user-driven plugin management but block GitHub-sourced marketplaces:
c.NotebookIntelligence.allow_github_plugin_import = FalseOr via env: NBI_ALLOW_GITHUB_PLUGIN_IMPORT=false (also accepts true/1/0/yes/no/on/off). When False, the "From GitHub" affordance in the Plugins panel hides itself and the backend rejects marketplace-add requests whose source is a GitHub URL, owner/repo shorthand, or git@github.com: reference. Local-path and arbitrary-URL sources remain available. This is finer-grained than claude_plugins_management_policy = force-off, which kills the entire surface.
Trust model. Plugins installed via
claude plugin installexecute as part of Claude Code sessions; NBI does not signature-verify or sandbox them, and theclaudeCLI's validation is best-effort. The marketplace-add path is a network fetch (server-side) — for multi-tenant or regulated deployments, default toclaude_plugins_management_policy = force-offand curate plugins server-side, or restrict marketplaces to vetted sources only.
When the marketplace source is a GitHub URL or owner/repo shorthand, NBI resolves a token with the same precedence as Skills' GitHub import:
GITHUB_TOKENenv var (server-process scope)GH_TOKENenv vargh auth tokensubprocess output (only ifghis on PATH)
Resolved tokens are injected into the claude plugin marketplace add subprocess via env, never argv — they do not appear in DEBUG logs. The chain is re-evaluated per call, so rotating the token (env update or gh auth refresh) takes effect on the next add. Required scope: repo for classic PATs, or contents:read on the target repo for fine-grained PATs. When gh is not installed the third step short-circuits silently; rely on GITHUB_TOKEN instead.
GitHub Enterprise: the detector recognizes GHE hosts that an admin declares via NBI_GITHUB_ENTERPRISE_HOSTS (CSV). Without that env, only public github.com is recognized, and a GHE marketplace URL falls through to anonymous git auth AND silently bypasses allow_github_plugin_import = False. Declare every host that should be treated as GitHub:
# Exact host matches only — safest default.
NBI_GITHUB_ENTERPRISE_HOSTS=github.acme.com,ghe.example.comTokens follow cookie-domain semantics: a bare token matches the exact host only; a leading-dot token (.acme.com) matches every subdomain of acme.com. Subdomain matching is opt-in because if suffix-matching were the default, declaring acme.com would silently inject GITHUB_TOKEN into any *.acme.com corp service (jira, artifactory, etc.) that someone happened to point marketplace-add at. Prefer the exact form; reach for the leading-dot form only when you actually have multiple GitHub subdomains under one apex.
# Explicit opt-in to subdomain matching: covers github.acme.com,
# ghe.acme.com, and any future *.acme.com GitHub-flavored host.
NBI_GITHUB_ENTERPRISE_HOSTS=.acme.comThe matcher rejects lookalikes that aren't actual subdomains, so github.acme.com.evil.test is correctly excluded regardless of the token shape.
This env is independent of NBI_GHE_SUBDOMAIN (which only configures GitHub Copilot's OAuth tenant). The two settings serve different surfaces; set whichever applies to your deployment.
For air-gap deployments, marketplace-add inherits the JupyterLab process env, so the same HTTPS_PROXY / HTTP_PROXY / NO_PROXY / NODE_EXTRA_CA_CERTS settings documented in Custom CA certs and corporate proxies apply. Pre-installed plugins (under ~/.claude/plugins/) keep loading without any network access.
c.NotebookIntelligence.terminal_drag_drop_policy = "force-off"Or via env: NBI_TERMINAL_DRAG_DROP_POLICY=force-off.
Force-off hides the per-terminal drag-drop toolbar toggle and rejects upload-staging POSTs from a terminal context. Drag-drop is enabled by default; flip it off in regulated tenants where the staging file write or the resulting @-mention path is undesirable.
c.NotebookIntelligence.refresh_open_files_on_disk_change_policy = "force-off"Or via env: NBI_REFRESH_OPEN_FILES_ON_DISK_CHANGE_POLICY=force-off.
The refresh watcher reloads open notebook and file editor tabs when their content changes on disk (for example, when an agent edits a file). It skips tabs with unsaved local edits. Enabled by default; users can opt out in the NBI Settings dialog. Use force-off in deployments where automatic reloads could surprise users editing the same files via external tooling, or force-on to mandate the behavior tenant-wide.
Terminal drag-drop and chat-sidebar file attach both write to the shared upload-staging directory under the JupyterLab process's tempfile.gettempdir(). Two tunables govern that endpoint:
| Env var | Default | Behavior |
|---|---|---|
NBI_UPLOAD_MAX_MB |
50 |
Per-file size cap (megabytes). Over the cap returns HTTP 413. 0 disables. |
NBI_UPLOAD_RETENTION_HOURS |
24 |
How long staged uploads survive before the next upload sweeps them. 0 keeps only the atexit purge (files survive the session). |
Trust model. Staged files live in the user's
tempfile.gettempdir()and inherit the directory's POSIX permissions. The samenbi-file-readdenylist that scopes general file reads does not apply to upload-staged files because they sit outside the Jupyter root. For sensitive-data tenants, setNBI_UPLOAD_RETENTION_HOURS=0to skip retention beyond the session and pair withterminal_drag_drop_policy = force-offto keep the surface to chat-sidebar attachments only.
JupyterHub spawn-time profiles can carry per-team config:
c.KubeSpawner.profile_list = [
{
"display_name": "Data Science (managed skills)",
"kubespawner_override": {
"environment": {
"NBI_SKILLS_MANIFEST": "https://manifests.internal/team-ds.yaml",
"NBI_MANAGED_SKILLS_TOKEN": {"valueFrom": {"secretKeyRef": {...}}},
},
},
},
{
"display_name": "ML Research (no managed skills)",
"kubespawner_override": {"environment": {"NBI_SKILLS_MANIFEST": ""}},
},
]The reconciler handles skill name collisions between manifests and user-authored skills per entry (managed entries skip user-authored skills with the same name). Across teams, if a user moves between profiles that ship different data-eda skills, they see whichever team's skill the active profile reconciled most recently. Keep team skill names distinct (team-ds-data-eda, team-ml-data-eda) to avoid surprises.
NBI_MANAGED_SKILLS_TOKEN (or the managed_skills_token traitlet) authenticates managed-skills GitHub operations (manifest fetch when hosted on github.com, commits-API probing, tarball downloads).
- Minimum scope:
contents:readon the org/repos that host the manifest and skill bundles. - Rotation: NBI reads the token from env on every reconcile cycle. Restart the pod (or reissue the env, if your KubeSpawner re-reads on resume) to rotate.
- 401/403 behavior: a single auth failure on a managed operation is logged and not retried with the fallback token chain when
NBI_MANAGED_SKILLS_TOKENis set. This keeps misconfiguration visible. The reconciler continues with remaining entries. - User-initiated imports do not see this token. They use
GITHUB_TOKEN→GH_TOKEN→ghCLI auth. Keep these separate so a misconfigured org token can't unintentionally apply to user imports.
When enable_chat_feedback = True, NBI emits a telemetry event in-process whenever a user gives thumbs-up/down feedback in chat. The event payload includes the rating, the prompt, the response, and the model.
The event is emitted in-process only. Nothing leaves the process unless you write a custom handler that listens for it. The payload shape is not currently considered stable API; if you build on it, pin to a specific NBI version.
To pipe feedback into your internal observability stack (Kafka, OTel collector), write an extension that registers a listener for the telemetry event and forwards it.
All routes live under /notebook-intelligence/. All require Jupyter authentication (XSRF token plus Jupyter login token) including the /copilot WebSocket upgrade, which now inherits Jupyter's WebSocketMixin + JupyterHandler so the same allow_origin and identity-provider checks that apply to REST handlers also apply to the chat WS endpoint. The labextension obtains these automatically. There is no admin-only route; access control runs through Jupyter Server itself.
| Route | Method | Purpose |
|---|---|---|
/notebook-intelligence/capabilities |
GET | Capabilities + tool/provider gate state. |
/notebook-intelligence/config |
GET/POST | Read or update user-scope config. |
/notebook-intelligence/update-provider-models |
POST | Refresh model list for a provider (e.g., Anthropic SDK refresh). |
/notebook-intelligence/mcp-config-file |
GET/POST | Read or write ~/.jupyter/nbi/mcp.json. |
/notebook-intelligence/reload-mcp-servers |
POST | Re-discover MCP servers without restarting JupyterLab. |
/notebook-intelligence/emit-telemetry-event |
POST | Used by the frontend to emit telemetry events (e.g., chat feedback). |
/notebook-intelligence/gh-login-status |
GET | GitHub Copilot login state. |
/notebook-intelligence/gh-login |
POST | Begin GitHub Copilot device-flow login. |
/notebook-intelligence/gh-logout |
GET | Sign out of GitHub Copilot. |
/notebook-intelligence/copilot |
WS | Streaming chat / inline-completion WebSocket. |
/notebook-intelligence/rules |
GET | List discovered rules. |
/notebook-intelligence/rules/<id>/toggle |
PUT | Toggle a rule's active field. |
/notebook-intelligence/rules/reload |
POST | Manually reload all rules. |
/notebook-intelligence/skills |
GET/POST | List or create skills. |
/notebook-intelligence/skills/context |
GET | Skill context info for the active workspace. |
/notebook-intelligence/skills/import/preview |
POST | Preview a GitHub-hosted skill before installing. |
/notebook-intelligence/skills/import |
POST | Install a GitHub-hosted skill (user-initiated). |
/notebook-intelligence/skills/reconcile |
POST | Run the managed-skills reconciler. Returns 409 if NBI_SKILLS_MANIFEST is unset. |
/notebook-intelligence/skills/reconciler/stop |
POST | Incident-response kill switch. Stops the background reconciler without a server restart. Idempotent. |
/notebook-intelligence/skills/<scope>/<name> |
GET/PUT/DELETE | Skill detail; managed skills are read-only. |
/notebook-intelligence/skills/<scope>/<name>/rename |
POST | Rename a skill (denied for managed skills). |
/notebook-intelligence/skills/<scope>/<name>/files |
GET/POST/DELETE | Skill bundle file ops. |
/notebook-intelligence/skills/<scope>/<name>/files/rename |
POST | Rename a file inside a skill bundle. |
/notebook-intelligence/upload-file |
POST | Upload a file to attach as chat context (size and retention governed by upload_max_mb / upload_retention_hours). |
/notebook-intelligence/claude-sessions |
GET | List Claude Code sessions for the working directory. |
/notebook-intelligence/claude-sessions/resume |
POST | Resume a Claude session. |
/notebook-intelligence/claude-mcp |
GET/POST | List or add Claude-mode MCP servers. Gated by claude_mcp_management_policy. |
/notebook-intelligence/claude-mcp/<scope>/<name> |
GET/DELETE | Get or remove a Claude-mode MCP server by scope (user/project/local) and name. |
/notebook-intelligence/plugins |
GET/POST | List or install Claude plugins. Gated by claude_plugins_management_policy. |
/notebook-intelligence/plugins/<scope>/<name> |
POST/DELETE | Enable or disable (POST with {"action": "enable"|"disable"}) or uninstall (DELETE) a plugin. |
/notebook-intelligence/plugins/marketplace |
GET/POST | List or add plugin marketplaces. GitHub-sourced adds are gated by allow_github_plugin_import. |
/notebook-intelligence/plugins/marketplace/<name> |
DELETE | Remove a plugin marketplace. |
The extension respects c.ServerApp.base_url. Behind JupyterHub at /user/<name>/ everything still works because JupyterLab proxies routes through the per-user base URL automatically.
| Condition | User-visible behavior | Where to look in logs |
|---|---|---|
| LLM provider unreachable | Chat shows "Thinking…", then a connection-error toast. | JupyterLab terminal (server stderr). |
| LLM 401 (bad or expired key) | Chat shows the provider's error message. | JupyterLab terminal; the provider's own SDK logs. |
| Claude CLI missing | Chat hangs on "Thinking…" in Claude mode; never returns. | JupyterLab terminal — claude-agent-sdk connect failure. |
| Claude CLI fails to start (path mismatch) | Same as above. | Same — set NBI_CLAUDE_CLI_PATH and restart JupyterLab. |
| MCP stdio server crashes | The server's tools disappear from @mcp chat participant. |
JupyterLab terminal — server's stderr. |
MCP npx -y package fetch fails (offline) |
Server fails to start; tools missing. | JupyterLab terminal. |
| Managed-skills manifest 5xx / DNS failure | Reconciler logs the error; existing managed skills remain installed. | JupyterLab terminal — skill_reconciler warning/error. |
| Managed-skills tarball fetch fails (per entry) | That entry stays at the previously installed version; others succeed. | JupyterLab terminal — per-entry error. |
NBI_MANAGED_SKILLS_TOKEN 401/403 |
Reconcile fails; loud log; does not fall back to the GITHUB_TOKEN chain. |
JupyterLab terminal. |
| Ruleset frontmatter is invalid YAML | Rule is skipped; others load. | JupyterLab terminal — rule_manager warning. |
| Encrypted token decrypt fails | The chat sidebar prompts the user to sign in again. | JupyterLab terminal. |
claude plugin install fails (network, auth) |
Plugin row stays unchanged; install button surfaces the CLI's stderr. | JupyterLab terminal (plugin_manager warning). |
claude plugin marketplace add from GHE |
Without NBI_GITHUB_ENTERPRISE_HOSTS, falls through to anonymous git auth and may fail without a clear message. |
JupyterLab terminal; see GHE caveat in GitHub auth for marketplace add. |
| Copilot model-list endpoint fails | Chat-model dropdown silently falls back to the hardcoded list. | JupyterLab terminal (github_copilot warning). |
Upload exceeds NBI_UPLOAD_MAX_MB |
Terminal drag-drop and chat-sidebar attach both return HTTP 413. | JupyterLab terminal; check upload_max_mb traitlet. |
NBI is tested against the JupyterLab and jupyter_server versions declared in pyproject.toml.
| NBI version | JupyterLab | jupyter_server | Python |
|---|---|---|---|
| 5.0.x | 4.x | 2.x | 3.10+ |
| 4.8.x | 4.x | 2.x | 3.10+ |
| 4.7.x | 4.x | 2.x | 3.10+ |
| 4.6.x | 4.x | 2.x | 3.10–3.12 |
| 4.5.x | 4.x | 2.x | 3.10–3.12 |
| 4.4.x | 4.x | 2.x | 3.10–3.12 |
| 4.3.x | 4.x | 2.x | 3.10–3.12 |
Upper bounds for litellm, claude-agent-sdk, anthropic, and mcp are not pinned in pyproject.toml. For production deployments, pin these in your image build:
pip install \
"notebook-intelligence==5.0.*" \
"litellm==1.83.*" \
"claude-agent-sdk==0.x.*" \
"anthropic==0.x.*" \
"mcp==1.27.*"Substitute the versions you've validated. As of 5.0.0 NBI uses the official mcp Python SDK (the prior fastmcp dependency was removed); see the 5.0.0 changelog migration note if your image previously pinned fastmcp. The NBI test suite is currently TypeScript-only (jlpm test); end-to-end Python testing is a future work item.
NBI's cryptography dependency is used solely to encrypt the stored GitHub Copilot token. The default password (nbi-access-token-password) and the encryption are intended for at-rest obfuscation, not as a FIPS-validated secret store.
If you operate under FIPS:
- Run with the FIPS-mode OpenSSL build of Python. NBI's
cryptographycalls (Fernet under the hood — AES-128-CBC plus HMAC-SHA256) work under FIPS-mode OpenSSL. - Set a per-user
NBI_GH_ACCESS_TOKEN_PASSWORDso the encryption key is not derivable. - For higher assurance, disable "remember GitHub Copilot login" entirely so no encrypted-at-rest token exists.
NBI itself does not assert FIPS compliance.
NBI's per-user memory cost depends on which Python dependencies actually get imported. A clean Jupyter Server with NBI installed but no LLM activity uses roughly the baseline of notebook_intelligence plus its server-imported dependencies.
We have not published measured numbers. If you size pods aggressively, profile your image: import everything NBI imports lazily (run a chat turn, trigger inline completion, exercise claude-agent-sdk) and measure RSS before sizing your pod memory request. Inline completion under load is the chattiest path; consider provider-side rate limits if you have hundreds of simultaneous users on a paid endpoint.
A measured-baseline document is on the roadmap. If you have numbers from a production deployment, share them in a GitHub issue.