Jupyter notebooks leak secrets in places ordinary code does not have: saved outputs, error tracebacks, embedded images, checkpoint copies and the IPython history database. A data scientist sets an API key in a cell, prints a configuration object to check it, and commits the notebook with the output still inside the JSON. Removing the line later does not remove the output, the checkpoint or the Git history. This runbook shows how to find secrets in .ipynb files on laptops, shared servers and repositories, using the notebook format itself, open source secret scanners, nbstripout and Git history scanning, and how to clean up without losing evidence. It is for application security, data platform and secrets management teams. For the server side, see the Jupyter Server Security Guide.
This is an independent guide from NHI Mgmt Group, based on the public documentation of the Jupyter, IPython and scanner projects as of November 2026. It is not sponsored or reviewed by any of them. Tools change, so confirm options against each project's current documentation.
Key takeaways
- Scan outputs, not just code. A notebook stores
stream,execute_result,display_dataanderroroutputs next to the source, so a printed token or a traceback with a connection string is saved even if the cell that set it is clean. - Look beyond the notebook file. Jupyter Server keeps checkpoint copies in a
.ipynb_checkpointsfolder beside each notebook, and IPython saves input history tohistory.sqlitein the profile directory. - Git history is the big exposure. nbstripout stops new outputs reaching Git but does not rewrite history; secrets already committed stay in past commits until you rotate them and rewrite.
- Regex scanners miss images. Screenshots in markdown attachments and
image/pngoutputs are base64 data, so a console screenshot with a key in it needs a human or OCR review. - Rotate first. A secret that reached a shared server or a repository is exposed; strip and rewrite after the credential is revoked.
When you need this
- Before publishing or open sourcing a notebook repository: GitHub secret scanning runs automatically and free on public repositories across the entire Git history, so a key you missed will be found by others too.
- When onboarding a shared notebook platform: home directories on a JupyterHub or a team server collect years of notebooks with cloud keys and database passwords.
- After a key leak traced to a data team: large language model and cloud keys are the usual targets. See LLMjacking 2024 to 2026 for how quickly leaked keys are abused.
- As a recurring control: NIST SP 800-53 IA-5(7) No Embedded Unencrypted Static Authenticators applies to analysis code as much as to applications.
Where secrets hide in a notebook
| Location | How the secret gets there | Found by text scanners? |
|---|---|---|
Code cell source | Literal key in a client constructor or %env NAME=value | Yes |
stream output (stdout, stderr) | print() of a config, debug logging, a library that echoes its settings | Yes, inside JSON strings |
execute_result and display_data text | Last expression returns a dict or DataFrame holding credentials; running %env with no argument lists all environment variables and their values | Yes |
error output | evalue and traceback include a connection string or signed URL | Yes |
Markdown attachments and image/png outputs | Pasted screenshot of a console or settings page | No, base64 image data |
Cell and notebook metadata | Extensions that store connection details or parameters | Yes |
.ipynb_checkpoints/ | Saved copy of the notebook, outputs included, created beside the file | Yes, if the folder is scanned |
~/.ipython/profile_default/history.sqlite | Every input line, including ones later deleted from the notebook | Only if you query or export it |
| Exports (HTML, PDF, scripts) | Rendered copies shared by email or in reports | Yes for text formats |
| Git history | Any earlier commit of the notebook | Yes, with a history scan |
The format matters for scanning. On disk, nbformat stores source as a single string or a list of strings, and outputs as JSON strings with escaped newlines and quotes. Most pattern-based detections still match a key inside a JSON string, but multi-line values such as private keys are split across list items and escaped, so a rule that expects the full PEM block may match only the header line. Treat a BEGIN ... PRIVATE KEY header as a finding even when the rest is not matched.
Before you start
- Scope: list the places notebooks live: repositories, laptops (through your endpoint tooling), shared servers and hub home directories, object storage used for notebook sharing, and hosted notebook services with their own storage.
- Tools: one or more of Gitleaks, TruffleHog and detect-secrets for text scanning, nbstripout for output detection and prevention, and Python with nbformat if you want to read specific fields.
- Findings handling: run scanners with redaction where available (Gitleaks
--redact) and store location, type and a fingerprint rather than the value. - Permission: scanning user home directories on a shared server reads other people's work; agree this with the platform owner and data protection lead first.
- Verification: TruffleHog can test whether a found credential is live by calling the provider's API. Use it only where your policy allows testing credentials you do not own.
Step-by-step
Steps 1 to 5 are read-only. Steps 6 to 8 change files, history or credentials.
- Inventory notebooks, checkpoints and history files. On a server or a copied directory, list
*.ipynbfiles including those under.ipynb_checkpoints, and everyhistory.sqliteunder the IPython directory ($HOME/.ipythonunlessIPYTHONDIRis set). In a repository,git ls-files '*.ipynb'lists tracked notebooks. Record owners from the file system or commit authors. - Find notebooks that carry outputs. nbstripout can report without changing anything:
nbstripout --dry-run path/to/*.ipynb nbstripout --verify path/to/*.ipynb--dry-runlists files that would be stripped;--verifyfails if any file would change and is the option the project recommends for CI. Notebooks with outputs go to the top of the review list. - Scan the files. Run a text scanner over the notebook tree, checkpoints included:
For detect-secrets, review results withgitleaks dir <notebook-dir> --redact --report-format json --report-path nb-findings.json trufflehog filesystem <notebook-dir> --results=verified,unknown --json detect-secrets scan <notebook-dir> --all-files > .secrets.baselinedetect-secrets audit .secrets.baseline. If notebooks are large because of embedded images, check Gitleaks'--max-target-megabytessetting so big files are not silently skipped. - Scan Git history. For each repository, scan every commit, not just the working tree:
gitleaks git <repo-path>ortrufflehog git file://<repo-path> --results=verified,unknown. On GitHub, secret scanning covers the whole history on all branches; review its alerts alongside your own results. The Git Secret Scanning Guide covers rule tuning. - Search IPython history and review images. In a session for the affected profile,
%hist -g *key*or%history -g *token*searches the full saved history with a glob pattern, which finds keys typed and later deleted. For notebooks with markdown attachments or image outputs, open them and review screenshots by eye, or extract them for OCR if you have that capability. - Rotate or revoke every confirmed secret. Identify the provider and the identity, check the provider's logs for use since the commit or file date, issue a replacement through your secrets manager and revoke the old value. Follow the Leaked Credential Response Playbook; for model provider keys see AI model provider API keys.
- Clean the files. Clear outputs with
jupyter nbconvert --clear-output --inplace <notebook>.ipynbor nbstripout, remove literal values from source, delete the matching.ipynb_checkpointscopies, and remove or archive the affectedhistory.sqliteafter you have captured any evidence you need. - Rewrite Git history where needed. nbstripout's documentation is explicit that it does not clean past commits and that rewriting with git-filter-repo affects the history of the repository. Coordinate with everyone who has a clone, rewrite, force-push and ask the hosting provider to purge cached views if the repository was public. See How to Remove Secrets From Git History. Rotation in step 6 is what actually removes the risk.
Choosing tools and approach
| Option | Best for | Trade-off |
|---|---|---|
Gitleaks dir and git | Fast pattern scans of files and history; SARIF and JSON output; redaction | No live verification; tuning needed for generic keys |
TruffleHog filesystem and git | Ranking findings by whether the credential is live | Verification calls the provider; needs policy approval |
| detect-secrets baseline | Teams that want to accept existing findings and block new ones | Baseline can hide old live secrets if nobody audits it |
nbstripout --dry-run and --verify | Finding notebooks with outputs; CI gate | Does not detect secrets in source cells or history |
| Platform secret scanning (for example GitHub) | Continuous coverage of hosted repositories | Only what is pushed; laptops and servers are out of scope |
A single analyst or small team can run Gitleaks and nbstripout in a pre-commit hook and be well covered. A large organisation with a shared hub should add a scheduled scan of home directories and checkpoints, route findings to notebook owners, and treat the hub's own credentials as a separate problem. Hosted notebook platforms are a concentration of keys: in ChaosDB 2021, a flaw in a notebook feature exposed primary database keys of thousands of customers.
Scanning a shared notebook server
A shared server or hub changes the job in four ways. Run as a dedicated identity: the scanner needs read access to every user's home directory, which makes it a privileged non-human identity; give it a named owner, read-only access to a snapshot or backup copy rather than live directories where possible, and no network route to the internet unless verification is approved. Scan the snapshot, not the live tree: users editing notebooks during the scan create checkpoint churn and partial files. Map findings to people: home directory ownership gives you the owner; send each person only their own findings, with the location and secret type but not the value. Schedule it: a weekly run of the same command against a fresh snapshot, compared with the previous report by fingerprint, shows new exposures rather than the whole backlog again. Gitleaks' .gitleaksignore file accepts finding fingerprints, so reviewed false positives stay suppressed between runs.
IPython history needs care on shared servers. Each user's history.sqlite holds every line typed in every session for that profile, including values pasted and then deleted from the notebook. It is a SQLite database rather than text, so file scanners may not read it reliably; search it from within IPython with %history -g, or export it with %history -g -f <file> and scan the export, then delete the export.
Verify it worked
- Credentials: the revoked value fails at the provider and the replacement is read from an environment variable or a secrets manager at run time.
- Files: a repeat scanner run over the directory, checkpoints included, returns no matching finding;
nbstripout --verifypasses. - History: a history scan of the rewritten repository no longer finds the value, and fresh clones are used.
- Prevention:
nbstripout --is-installedreturns 0 in the repository, or--statusshows the filter.
Rollback and common errors
| Situation | Fix |
|---|---|
| Outputs needed for a report were cleared | Re-run the notebook with the new credential; keep outputs out of Git, or use nbstripout --keep-output only for reviewed repositories |
| A job breaks after rotation | Point it to the new value in the secrets manager; do not restore the old key |
| Filter installed but secrets still committed | The filter strips outputs, not source; add a secret scanning pre-commit hook |
| Scanner finds nothing in a large notebook | File skipped by a size limit; raise --max-target-megabytes or scan with another tool |
| History rewrite rejected | Branch protection blocks force-push; agree a window with the repository owner |
Common failure patterns
- Deleting the cell and moving on: the output, the checkpoint copy, the IPython history and every earlier commit still hold the value.
- Running
%envto check a variable: the bare form lists every environment variable with its value into the saved output. - Relying on
.gitignorefor checkpoints alone: it keeps them out of Git but they remain on shared servers and backups. - Accepting a detect-secrets baseline without auditing it: existing live keys become "known" and are never rotated.
- Sharing HTML exports: rendered notebooks carry the same outputs as the source file to a wider audience.
Hardening afterwards
- Install the output filter:
nbstripout --install --attributes=.gitattributesputs the configuration in a versioned file, andnbstripout --verifyin CI catches anyone without the filter. - Load secrets at run time: read keys from environment variables set outside the notebook, or from a secrets manager client, and never display the value. See Environment Variables and .env Files.
- Prefer workload identity: on managed platforms, give the kernel a cloud identity instead of a static key. See Dynamic Secrets and Ephemeral Credentials Guide.
- Add a pre-commit scan: Gitleaks or detect-secrets hooks block new secrets in source cells.
Metrics
| Metric | Direction |
|---|---|
| Repositories with notebooks and no output filter or CI check | Zero |
| Open secret findings in notebooks, checkpoints and history | Falling |
| Median time from finding to revocation | Hours |
| Findings confirmed live by verification | Falling each quarter |
| Notebooks committed with outputs | Falling to reviewed exceptions only |
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev 5 | IA-5(7) No Embedded Unencrypted Static Authenticators; CM-12 Information Location; SC-28 Protection of Information at Rest | No literal secrets in notebooks; know where copies live; protect stored files |
| NIST CSF 2.0 | PR.AA-01; PR.DS-01 | Manage service credentials; protect data at rest including notebook stores |
| ISO/IEC 27001:2022 Annex A | 5.17 Authentication information; 8.12 Data leakage prevention | Secret handling and leak detection |
| MITRE ATT&CK | T1552.001 Credentials In Files | Attackers search files on compromised hosts and repositories for stored credentials |
Practitioner checklist
- Inventory notebooks,
.ipynb_checkpointsfolders andhistory.sqlitefiles in scope. - List notebooks with outputs using
nbstripout --dry-run. - Scan files and checkpoints with a redacting secret scanner.
- Scan the full Git history of every notebook repository.
- Search IPython history and review embedded images by eye.
- Rotate or revoke every confirmed secret before cleaning.
- Clear outputs, delete checkpoints and rewrite history where the value was committed.
- Install nbstripout with a versioned attributes file and add
--verifyto CI.
Standards and references
- Jupyter: The Notebook file format
- nbstripout: project documentation
- IPython: Built-in magic commands
- IPython: IPython reference
- GitHub Docs: About secret scanning
- NIST: SP 800-53 Rev. 5
- MITRE ATT&CK: Unsecured Credentials: Credentials In Files
- TruffleHog: project documentation
Related NHI Mgmt Group resources: Jupyter Server Security Guide · MLOps Security Guide · Git Secret Scanning Guide · How to Remove Secrets From Git History