# Jupyter notebooks

A notebook is the easiest place in a codebase to leak a credential. The two
usual ways happen without anyone deciding to do anything careless:

- **A token pasted into a cell.** A kernel launched from JupyterLab, VS Code, or
  JupyterHub doesn't inherit the shell where you exported anything, so the
  obvious fix is to type the credential into the first cell — where it is saved
  into the `.ipynb` and committed.
- **A secret printed by a cell.** Notebook files store *outputs* as well as
  source. `print(api_key)`, a stray `secrets` on the last line, or a DataFrame
  built from a connection string all write plaintext into the file that gets
  pushed and reviewed.

The Python SDK's `load()` is built for exactly this. One call at the top of the
notebook, and the secrets land in `os.environ` where your libraries already look
for them:

```bash
pip install seekrit
```

```python
import seekrit

seekrit.load()
```

That's the whole integration. Everything downstream reads the environment as
usual — `psycopg`, `boto3`, `openai`, `sqlalchemy`, anything that honors env
vars:

```python
import os, psycopg

conn = psycopg.connect(os.environ["DATABASE_URL"])
```

> **Note:** `load()` needs a [service token](/docs/guides/service-tokens) — a machine credential bound to exactly one app environment. That scope is the notebook's blast radius, so create a token for the environment the analysis should see (a read-only `staging`, usually) rather than reusing a production one.

## Where the token comes from

`load()` looks in three places, in order:

1. the `token=` argument,
2. `$SEEKRIT_TOKEN` in the kernel's environment,
3. an interactive prompt.

The prompt is the point. When no token is configured, `load()` asks for one
through a password field — ipykernel routes `getpass` to the notebook
frontend — so the credential goes into the kernel's memory and **never into the
notebook file**:

```python
seekrit.load()
# seekrit service token (skt_...):  [·······]
# <seekrit: 7 secrets loaded from acme/analytics/staging: API_KEY, DATABASE_URL, …>
```

To skip the prompt, give the kernel the token in its environment. Launching
Jupyter under [`seekrit run`](/docs/guides/run) is the tidiest way — the kernel
inherits it, and `load()` finds it without asking:

```bash
SEEKRIT_TOKEN=skt_... seekrit run -- jupyter lab
```

Control the prompt explicitly when you need to:

```python
seekrit.load(prompt=False)   # never ask; raise if no token is configured
seekrit.load(prompt=True)    # require the prompt
```

For a notebook executed headlessly — `papermill`, `nbconvert --execute`, a
scheduled job — there is no frontend to prompt, so set `SEEKRIT_TOKEN` in the
environment that runs it. `load()` says so rather than hanging.

## Cell outputs stay clean

`load()` deliberately does not return your secrets. Its result carries the
**names** it loaded and the scope they came from, and nothing else — so the
value that gets saved into the `.ipynb` when the cell displays it is a summary:

```python
seekrit.load()
# <seekrit: 7 secrets loaded from acme/analytics/staging: API_KEY, DATABASE_URL, …>
```

There is no `result["API_KEY"]`; the object has no way to hand back a value. Read
them from the environment instead, and keep them out of the last line of a cell:

```python
loaded = seekrit.load()

len(loaded)                  # 7
"DATABASE_URL" in loaded     # True
sorted(loaded)               # ['API_KEY', 'DATABASE_URL', …]
```

> **Warning:** This guards the *summary*, not your own cells. `print(os.environ["API_KEY"])` still writes a secret into the notebook file, and so does an exception traceback that renders a connection string. Clear outputs before committing — `jupyter nbconvert --clear-output --inplace notebook.ipynb`, or [`nbstripout`](https://github.com/kynan/nbstripout) as a git filter — and treat a notebook that has ever displayed a secret as needing a [rotation](/docs/guides/rotation).

## Re-running the cell

`load()` refreshes: run the cell again and every name is re-resolved and
overwritten, which is what you want after rotating a secret or switching
branches. That is the opposite of `Client().into_env()`, which leaves existing
variables alone — calling `load()` is a statement that seekrit owns these
names. To keep what the kernel already has:

```python
seekrit.load(override=False)   # existing os.environ wins; skipped names are listed
```

## Options

Everything the [SDK client](/docs/guides/sdks) takes, `load()` takes too:

```python
seekrit.load(
    token=None,          # default: $SEEKRIT_TOKEN, then prompt
    api_url=None,        # default: $SEEKRIT_API_URL
    overrides={"shared": "dev"},   # pull another env slice of a composed group
    env=None,            # default: os.environ
    override=True,       # resolved secrets win
    prompt="auto",       # "auto" | True | False
    timeout=30.0,
    interpolate=True,    # expand ${OTHER_SECRET} references
)
```

`load()` is fail-closed like the rest of the SDK: a bad token, an unreachable
API, or a layer that won't decrypt raises instead of loading a partial
environment.

## Colab, Kaggle, and other hosted kernels

The same call works — the prompt is the mechanism that makes it safe. Hosted
notebooks have no shell you control, so `$SEEKRIT_TOKEN` won't be set and
`seekrit.load()` will ask for the token each session:

```python
!pip install --quiet seekrit
import seekrit; seekrit.load()
```

Prefer the platform's own secret store for the token itself where one exists
(Colab's **Secrets** panel, Kaggle's **Add-ons → Secrets**) and pass it through,
so a shared notebook prompts its reader rather than carrying your credential:

```python
from google.colab import userdata
seekrit.load(token=userdata.get("SEEKRIT_TOKEN"))
```

## What the notebook can and can't do

The SDK is **read-only**: it resolves and decrypts. Creating, editing, and
rotating secrets stays in the [dashboard](/docs/guides/web-app) and
[CLI](/docs/guides/cli), which use a human principal's passphrase-protected key.
A notebook holding a service token can read one environment's secrets and
nothing else.

Decryption happens in the kernel. The API returns ciphertext plus a data key
wrapped to your token's public key, and the SDK unwraps and decrypts locally —
the same path as every other seekrit client. See the [encryption
model](/docs/concepts/encryption).
