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
.ipynband committed. - A secret printed by a cell. Notebook files store outputs as well as
source.
print(api_key), a straysecretson 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:
pip install seekrit
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:
import os, psycopg
conn = psycopg.connect(os.environ["DATABASE_URL"])
load() needs a service token — 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:
- the
token=argument, $SEEKRIT_TOKENin the kernel's environment,- 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:
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 is the tidiest way — the kernel
inherits it, and load() finds it without asking:
SEEKRIT_TOKEN=skt_... seekrit run -- jupyter lab
Control the prompt explicitly when you need to:
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:
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:
loaded = seekrit.load()
len(loaded) # 7
"DATABASE_URL" in loaded # True
sorted(loaded) # ['API_KEY', 'DATABASE_URL', …]
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 as a git filter — and
treat a notebook that has ever displayed a secret as needing a
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:
seekrit.load(override=False) # existing os.environ wins; skipped names are listed
Options
Everything the SDK client takes, load() takes too:
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:
!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:
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 and 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.