← All articles

Secrets in Jupyter Notebooks: How Data Science Workflows Leak Credentials Into the Wild

July 10, 2026

The Notebook Is Not Just Code — It's a Credential Archive

Jupyter notebooks are one of the most collaborative formats in modern software development. Data scientists share them on GitHub, embed them in documentation, and export them as HTML reports. That openness is exactly what makes them a dangerous credential leak vector that most security tooling is tuned to miss.

Unlike a plain .py file, a .ipynb file is a JSON document. It stores not only your source code but also every execution output your session produced — printed values, tracebacks, and API responses included. That means a single print(api_key) during a debugging session bakes your secret into the file, permanently, unless you manually clear outputs before committing.

Exactly How Credentials End Up in Notebooks

1. Output Cells That Print Sensitive Values

The most common scenario: a developer is debugging a failing API call and temporarily prints the key or response headers to confirm the request is being formed correctly.

# Debugging authentication
import os
api_key = os.environ["OPENAI_API_KEY"]
print(api_key)   # "Just for a second" — but the output cell commits with it

The variable may come from an environment variable, but the rendered output is now a plain-text secret stored inside the notebook's JSON structure under outputs[].text. Standard secret scanners that look at source code lines will skip it entirely.

2. Credentials Passed Directly Into API Calls

Early-stage notebooks often evolve from scratch pads. Hardcoded keys appear as temporary values that never get cleaned up:

import openai
openai.api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"   # "I'll move this later"

The key lives in the source cell. When the notebook is committed and pushed — even to a private repo — it's now in Git history forever.

3. Error Tracebacks Containing Auth Headers

When an HTTP library like requests throws an exception, the traceback sometimes includes the full request object, which may contain Authorization headers with bearer tokens. These appear in the output cell automatically, without any explicit print statement from the developer.

4. Notebooks Exported and Shared as HTML or PDF

Teams frequently run jupyter nbconvert --to html my_analysis.ipynb to share results with stakeholders. Every output cell renders in that HTML — including anything that printed a token. These files land in shared drives, email threads, or internal wikis with no secrets-scanning coverage at all.

Why Notebook Secrets Are Especially Hard to Catch

  • Non-standard file structure: Most regex-based scanners target source lines in .py, .js, and .ts files. The leaked value in a notebook lives in a JSON field several levels deep — cells[n].outputs[m].text[k] — and requires a scanner that understands the .ipynb format.
  • High diff noise: Notebooks update cell execution counts and metadata on every run, producing large, noisy diffs. Developers often commit them without reviewing, making a leaked secret easy to miss in review.
  • Shared kernels and remote execution: Notebooks running on JupyterHub, Google Colab, or SageMaker often store outputs server-side before developers download and commit them, adding another path for credential exposure.
  • Git history persistence: Clearing the outputs now does not remove the secret from earlier commits. Anyone who can access the repo history can retrieve it.

A Practical Remediation Checklist

Step 1: Scan Your Notebooks Now — Including Output Cells

Run a scanner that reads the full JSON structure of .ipynb files, not just the source fields. Verify it checks outputs[].text and outputs[].data in addition to source blocks. To run a free GhostCred scan across your repos and surface credentials hiding in notebook outputs, execution traces, and config files in under 60 seconds.

Step 2: Strip Outputs Before Every Commit

Make output clearing automatic rather than a manual discipline. Install nbstripout as a Git filter:

pip install nbstripout
nbstripout --install   # installs the Git filter into .git/config

With this filter active, Git automatically strips all output cells from notebooks at commit time. The working copy retains outputs for your local development session; the committed file never contains them.

Step 3: Add a Pre-Commit Hook for Secret Detection

Even with nbstripout, source cells may still contain hardcoded keys. Add a pre-commit check:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

Run pre-commit install to activate it. This catches hardcoded keys in source cells before they reach your remote.

Step 4: Rotate Any Key That Appeared in a Committed Output

Clearing the output and pushing a new commit is not remediation. The secret exists in Git history and may have already been indexed by public repo crawlers. Rotate the credential immediately:

  1. Generate a new key in the provider's console (AWS IAM, OpenAI, Stripe, etc.).
  2. Update every service, environment variable, and secret manager entry that uses the old key.
  3. Revoke the old key — do not just stop using it.
  4. Audit provider access logs for any unexpected usage during the exposure window.

Step 5: Purge the Secret From Git History

If the notebook was pushed to a repository, rewriting history removes the secret from future clones but does not protect anyone who cloned before the rewrite. Use git filter-repo (preferred over the deprecated filter-branch):

pip install git-filter-repo
git filter-repo --path path/to/notebook.ipynb --invert-paths

Force-push the rewritten history and contact your Git hosting provider's support team to purge cached views if the repository is public.

Step 6: Load Secrets From a Vault, Not the Environment Directly

The root cause is secrets entering the notebook's execution environment in ways that make accidental disclosure easy. Instead of reading from os.environ inline, centralise secret access:

# Using python-dotenv to load from a .env file excluded by .gitignore
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv("MY_API_KEY")

For team environments, prefer a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) with short-lived credentials scoped to the minimum permissions required for the notebook's task.

Organisational Controls Worth Adding

  • Repository policy: Block commits to main that include .ipynb files with non-empty output cells via a branch protection rule and a CI check.
  • Periodic history scanning: Schedule automated scans of your full Git history, not just the latest commit. Secrets from months-old debug sessions still carry risk if the key was never rotated.
  • Onboarding documentation: Add notebook hygiene — nbstripout installation, no hardcoded keys — to your engineering onboarding checklist. Most exposure happens because developers were never told the risk existed.

The Broader Pattern

Jupyter notebooks are one example of a wider class of rich document formats — including RMarkdown, Observable notebooks, and Databricks export files — where execution outputs become part of the committed artefact. Any tooling or policy you apply to .ipynb files should prompt you to ask: what other document formats in our workflow store rendered output alongside code?

The answer to credential exposure in notebooks is not to stop using notebooks. It is to treat their output cells with the same discipline you would apply to any file that might contain a secret — because after one careless commit, it does.

See what's exposed in your own code.

Run a free scan