Skip to main content

CLI tool for LLM agents to operate Jupyter Lab servers

Project description

jupyter-jcli

CLI tool for LLM agents to operate Jupyter Lab servers.

j-cli enables AI agents (and humans) to remotely control Jupyter servers — execute code in kernels, manage sessions, and write outputs back to notebooks, all from the command line.

Installation

# latest release
uv tool install jupyter-jcli

# latest dev version
uv tool install git+https://github.com/tttpob/jcli.git

# verify the installed CLI
j-cli --version

Requires Python 3.10+.

Note: the PyPI package name is jupyter-jcli, while the installed binary is j-cli.

Recommended Workflow

1. Set up environment variables

Use direnv so the env vars are loaded automatically whenever you enter the project directory:

# .envrc
export JCLI_JUPYTER_SERVER_URL=http://localhost:8888
export JCLI_JUPYTER_SERVER_TOKEN=your-token
direnv allow

2. Launch Jupyter

# stdout is pipe-safe — the hint line goes to stderr
$(j-cli serve-cmd --serve-backend lab)

This prints (and immediately executes) a command like:

jupyter lab --ServerApp.token="$JCLI_JUPYTER_SERVER_TOKEN" \
    --ServerApp.ip=localhost --ServerApp.port=8888 --no-browser

The token value is never inlined; it is always referenced as $JCLI_JUPYTER_SERVER_TOKEN.

3. Verify connectivity

j-cli healthcheck

4. Set up hooks (once per project)

Install Claude Code hooks so the AI redirects notebook edits through j-cli:

j-cli setup claude

Install the git pre-commit hook to keep .py / .ipynb pairs in sync:

j-cli setup git

If your notebooks live in a subdirectory, limit pair detection to that path (avoids false positives elsewhere in the repo). --include can be repeated:

j-cli setup git --include "notebooks/*"
# or multiple directories
j-cli setup git --include "notebooks/*" --include "experiments/*"

Commands

Global Options

Flag Description
-s, --server-url Jupyter server URL (env: JCLI_JUPYTER_SERVER_URL, default: http://localhost:8888)
-t, --token Auth token (env: JCLI_JUPYTER_SERVER_TOKEN)
-j, --json Output as JSON for programmatic use
--version Show version

healthcheck

Check server connectivity and running kernel count.

j-cli healthcheck

kernelspec list

List available kernel specifications.

j-cli kernelspec list

session

j-cli session create --kernel python3 --name my-session
j-cli session list
j-cli session kill <session_id>

kernel

j-cli kernel interrupt <session_id>
j-cli kernel restart <session_id>

setup claude

Install Claude Code hooks (PreToolUse and PostToolUse) that intercept notebook-execution bypass tools and keep .py / .ipynb pairs in sync, redirecting Claude to use j-cli instead.

j-cli setup claude           # default: .claude/settings.local.json (gitignored)
j-cli setup claude --project # .claude/settings.json (committed, team-shared)
j-cli setup claude --user    # ~/.claude/settings.json (global, all projects)

# remove all j-cli managed hooks from the target file
j-cli setup claude --remove
j-cli setup claude --project --remove

The install command is idempotent — re-running updates hooks in place without duplicating them. --remove prunes only j-cli managed entries, preserving any unrelated user hooks. If the settings file becomes empty after removal it is deleted.

setup git

Install a pre-commit hook shim that runs j-cli _hooks pre-commit-pair-sync and update .gitignore to exclude paired .ipynb files.

j-cli setup git              # default: .githooks/pre-commit + set core.hooksPath
j-cli setup git --local      # .git/hooks/pre-commit (this clone only)
j-cli setup git --include "src/*.py"  # only sync matching files

# remove the managed hook and gitignore block
j-cli setup git --remove
j-cli setup git --local --remove

--remove deletes the hook only if it was written by j-cli, leaves core.hooksPath alone if it points to a non-j-cli directory, and removes the managed .gitignore block. Unrecognised hooks are skipped with a warning.

setup codex

Install Codex hooks (PreToolUse and PostToolUse) that intercept notebook-execution bypass tools and keep .py / .ipynb pairs in sync, redirecting Codex to use j-cli instead.

j-cli setup codex             # writes .codex/hooks.json (default)
j-cli setup codex --project   # same as default
j-cli setup codex --user      # writes ~/.codex/hooks.json (global, all projects)

# remove all j-cli managed hooks from the target file
j-cli setup codex --remove
j-cli setup codex --project --remove

Prerequisites: Codex hooks require [features]\ncodex_hooks = true in .codex/config.toml. setup codex checks for this and warns if missing. See Codex hooks docs.

The install command is idempotent — re-running updates hooks in place without duplicating them. --remove prunes only j-cli managed entries, preserving any unrelated user hooks.

What gets installed (4 hooks):

Hook Event Trigger Action
notebook-exec-guard PreToolUse (Bash) jupyter nbconvert --execute, papermill, runipy, ipython <.ipynb> Hard deny, redirect to j-cli
python-run-guard PreToolUse (Bash) Bash command targeting a .py with a paired .ipynb Soft deny, suggest j-cli session
pair-drift-guard-pre PreToolUse (apply_patch) apply_patch touching a paired .py / .ipynb Detect drift, auto-merge, deny stale edits
pair-drift-guard-post PostToolUse (apply_patch) After apply_patch completes Auto-sync the other side of the pair

notebook-edit-guard is not installed for Codex — Codex has no NotebookEdit tool; file edits go through apply_patch instead.

serve-cmd

Print a copy-pasteable Jupyter launch command that references the token via an environment variable rather than inlining it.

# set env vars (token is never echoed to the terminal)
export JCLI_JUPYTER_SERVER_URL=http://localhost:8888
export JCLI_JUPYTER_SERVER_TOKEN=your-token

j-cli serve-cmd --serve-backend lab
# → jupyter lab --ServerApp.token="$JCLI_JUPYTER_SERVER_TOKEN" \
#       --ServerApp.ip=localhost --ServerApp.port=8888 --no-browser

# override host / port / root dir
j-cli serve-cmd --serve-backend lab --ip 0.0.0.0 --port 9000 --root-dir /work

# remove --no-browser (useful for desktop Jupyter)
j-cli serve-cmd --serve-backend notebook --browser

# JSON output (for programmatic use)
j-cli -j serve-cmd --serve-backend server

The hint line (# paste this into a shell …) is written to stderr so the command itself can be used safely in $() substitution. The token reference "$JCLI_JUPYTER_SERVER_TOKEN" is always a literal shell variable reference — the actual token value is never inlined.

--serve-backend must be one of lab, server, or notebook.

vars

Inspect variables in a kernel session.

# list all variables (NAME / TYPE / VALUE table)
j-cli vars <session_id>

# inspect a single variable
j-cli vars <session_id> --name x

# rich inspection (MIME-typed data, DAP kernels only)
j-cli vars <session_id> --name x --rich

# JSON output for programmatic use
j-cli -j vars <session_id>
j-cli -j vars <session_id> --name x

Source: when the kernel advertises debugger support (kernel_info_reply.supported_features contains "debugger"), the DAP inspectVariables control-channel path is used (source="dap"). Otherwise a shell-channel code snippet is executed (source="fallback").

Ordering caveat: variables are returned in first-definition order (CPython dict insertion order). Re-assigning a variable does not move it to the end; only del x; x = … does. Do not infer recency from position in the list.

No mtime: the Jupyter debug protocol does not expose per-variable last-modified timestamps. No mtime or last_execution_count field is available in the protocol.

session list variable preview

By default, session list fetches a short variable preview for each idle kernel:

j-cli session list            # includes VARS column (default)
j-cli session list --no-vars  # faster, skips variable fetch
j-cli session list --vars     # force fetch even when >10 sessions

Each session row gets a VARS column showing the first 5 variable names. A hint line at the bottom points at j-cli vars <SESSION_ID> for the full list.

In JSON mode (-j), each session object gains a vars_preview key:

{"session_id": "...", "vars_preview": {"names": ["x", "df"], "total": 2}}

exec

Execute code in a kernel session. Supports inline code, py:percent files, and Jupyter notebooks.

# inline code
j-cli exec <session_id> --code "import pandas as pd; df = pd.read_csv('data.csv'); df.head()"

# execute from py:percent file
j-cli exec <session_id> --file analysis.py

# execute specific cells from a notebook
j-cli exec <session_id> --file notebook.ipynb --cell 0:3

# execute a single cell
j-cli exec <session_id> --file notebook.ipynb --cell 5

Cell spec formats (0-indexed):

Spec Meaning
3 Cell 3 only
3:7 Cells 3, 4, 5, 6
3: Cell 3 to end
:5 Cells 0 through 4

Notebook writeback: When executing from a py:percent file (one with # %% cell markers or a # --- front matter block), outputs are automatically written back to the paired .ipynb. If analysis.ipynb does not yet exist, j-cli creates it automatically. Plain Python scripts without markers are executed normally without creating a notebook.

Convert baseline refresh: When j-cli convert syncs a canonical managed pair (foo.pyfoo.ipynb, or foo.dummy.pyfoo.ipynb) inside a git repo, it also refreshes the sticky pair baseline under refs/jcli/pair-sync/*. This lets later drift checks compare against the last successful pair sync instead of falling back to an older HEAD.

If you convert to a non-canonical output path such as foo.py -> custom.ipynb or nb.ipynb -> custom.py, j-cli treats that as an export/conversion only and does not refresh the sticky baseline.

Troubleshooting Hooks

If a hook appears to run but produces no visible effect (silent exit 0 with no sync, no deny message), enable the per-hook debug log to capture stdin/stdout/stderr.

Edit .claude/settings.local.json and append --debug to the hook command you want to inspect, e.g.:

"command": "j-cli _hooks pair-drift-guard-post --debug"

Trigger the hook, then inspect the log:

ls /tmp/jcli-$UID/
cat /tmp/jcli-$UID/pair-drift-guard-post-*.log | jq .

Each invocation writes one JSON file containing the incoming payload, outgoing decision (if any), stderr, exit code, and any exception. Remove --debug when done — log files accumulate in /tmp and are not rotated.

Override the log directory with JCLI_DEBUG_LOG_DIR=/path/to/dir if /tmp is not writable or you want the logs elsewhere.

If refs/jcli/pair-sync/* accumulates over time, clean stale entries with:

j-cli _hooks gc-pair-sync-refs
j-cli _hooks gc-pair-sync-refs --dry-run

Py:Percent Format

j-cli supports the py:percent format — plain Python files with cell markers:

# ---
# jupyter:
#   kernelspec:
#     name: python3
# ---

# %%
import numpy as np

# %%
x = np.random.randn(100)
print(x.mean())

Development

# install with test dependencies
uv sync --extra test

# run tests (requires a real Jupyter server, started automatically by fixtures)
uv run pytest -v

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jupyter_jcli-0.4.4.tar.gz (285.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jupyter_jcli-0.4.4-py3-none-any.whl (65.8 kB view details)

Uploaded Python 3

File details

Details for the file jupyter_jcli-0.4.4.tar.gz.

File metadata

  • Download URL: jupyter_jcli-0.4.4.tar.gz
  • Upload date:
  • Size: 285.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.10 {"installer":{"name":"uv","version":"0.11.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for jupyter_jcli-0.4.4.tar.gz
Algorithm Hash digest
SHA256 9424f0ccf725338459366b8822f7a49907151665cb4d645e8df69dacfd278468
MD5 7833801dae5e8520d18f11b2e974f361
BLAKE2b-256 38f56bda74aeea0147c0fde8152b4ba046a507610d68c28e3b16e9c1fc57943f

See more details on using hashes here.

File details

Details for the file jupyter_jcli-0.4.4-py3-none-any.whl.

File metadata

  • Download URL: jupyter_jcli-0.4.4-py3-none-any.whl
  • Upload date:
  • Size: 65.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.10 {"installer":{"name":"uv","version":"0.11.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for jupyter_jcli-0.4.4-py3-none-any.whl
Algorithm Hash digest
SHA256 56275eae0328db746685ce219776e288a74bd7cddfca59ac26200261eef6d0e4
MD5 69e8a41c7fff4861f136ec636b6bf6fa
BLAKE2b-256 8753f65a5ba2940a81b4873c8059d25c629f0f48a3362cf70b05ffa81ac657b2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page