> Scans a Python recipe to find every place an environment variable is accessed — including `os.environ.setdefault(\"V\", \"d\")`, whose \"d\" would otherwise be a hidden default a user editing .env.example has no way to discover — then ensures all variables are declared in `.env.example`, that `load_dotenv()` is bootstrapped in the package `__init__.py`, and that `python-dotenv>=1.0.0` is listed in `pyproject.toml`. When a new entry is added to `.env.example`, the extracted default from sour...
Scanned 9/12/2026
Install to Claude Code
npx -y skills add thedixitjain/the-mega-skill-library --skill extract-python-environment-variables --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Extract Python Environment Variables?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/thedixitjain-extract-python-environment-variables)More formats (shields.io, HTML) on the badges page.
---
name: extract-python-environment-variables
description: "> Scans a Python recipe to find every place an environment variable is accessed — including `os.environ.setdefault(\"V\", \"d\")`, whose \"d\" would otherwise be a hidden default a user editing .env.example has no way to discover — then ensures all variables are declared in `.env.example`, that `load_dotenv()` is bootstrapped in the package `__init__.py`, and that `python-dotenv>=1.0.0` is listed in `pyproject.toml`. When a new entry is added to `.env.example`, the extracted default from source is written as the value (with a `# extracted-by:extract-env-vars` marker and provenance comment); values that look like stubs (`\"my-project-id\"`, `\"changeme\"`, `\"<...>\"`) are downgraded to the TODO placeholder but the source string is preserved in the marker comment. Also detects hardcoded model-name string literals (e.g. `\"gemini-3.5-flash\"` in `agent.py`) and rewrites them to bare..."
category: ai-agents-and-harness
source_repo: google/adk-samples
source_path: ".agents/skills/extract-python-environment-variables/SKILL.md"
source_url: https://github.com/google/adk-samples/blob/HEAD/.agents/skills/extract-python-environment-variables/SKILL.md
---
# Extract Python Environment Variables
Use this skill to ensure a Python recipe properly declares and loads all
environment variables it uses.
---
## What This Skill Does
Runs `scripts/extract_env_vars.py` against a recipe directory. The script:
1. **Scans** all `.py` files (excluding `tests/` and common cache/venv
directories) for environment variable accesses:
- `os.environ["VAR"]`
- `os.environ.get("VAR")` / `os.environ.get("VAR", "default")`
- `os.getenv("VAR")` / `os.getenv("VAR", "default")`
- `os.environ.setdefault("VAR", "default")` *(v2)*
Only names matching `^[A-Z_][A-Z0-9_]*$` (UPPER_SNAKE_CASE) are captured;
a lowercase name like `os.getenv("my_api_key")` is **skipped** and a
`[WARN]` line lists any that were dropped. Rename such vars to uppercase
in source, or add them to `.env.example` by hand.
2. **Updates `.env.example`** — appends any variables not already
declared AND upgrades stale TODO entries in place. Existing
user-authored lines are always preserved (see Rule 1 below).
- Creates `.env.example` from scratch if it does not exist.
- Each new/upgraded line has the shape:
```
VAR=<value> # extracted-by:extract-env-vars; <provenance>
```
- `<value>` is the **resolved default** extracted from source when the
author committed to one — with `os.environ.setdefault(...)` beating
`os.getenv("V", "d")` / `os.environ.get("V", "d")` (setdefault is a
stronger commitment: it mutates the process environment, while getenv
fallback is per-read). Alphabetical file order breaks ties.
- Values that look like **stubs** are downgraded to
`<TODO: update-this-value>` — e.g. `my-project-id`, `your-api-key`,
`changeme`, `<...>`, anything containing `example.com`. The source
string is preserved in the marker comment so the maintainer sees
what was found and can fix the source too.
- When no source supplied a string-literal default, the value is
`<TODO: update-this-value>` with a `no default in source` note.
- **Stale TODO upgrade (v2.1):** if an existing entry is either a
skill-authored TODO (has the marker) or a bare v1-era TODO (exact
value `<TODO: update-this-value>`, no marker, no inline comment)
AND source can now supply a real default, the line is rewritten in
place. This closes the discoverability loop for recipes originally
processed by v1 (which always wrote TODOs regardless of what source
said).
3. **Injects `load_dotenv()`** into the package `__init__.py` (the first
subdirectory inside the recipe that contains an `__init__.py`, skipping
`tests/` and hidden directories). The snippet injected is:
```python
from dotenv import load_dotenv
# Load variables from .env if present. In production the environment is
# already populated by the platform (Cloud Run, GKE, etc.), so a missing
# .env is expected and not an error.
load_dotenv()
```
If `load_dotenv` is already present the injection is skipped.
**Additionally — always, regardless of whether we injected** — appends
`# noqa: E402 -- must come after load_dotenv()` to any top-level relative
import (`from .x import y`) that sits AFTER a non-import module-level
statement. Two cases this covers:
- **Fresh injection.** The injected `load_dotenv()` call pushes
pre-existing trailing relative imports below a non-import statement, so
they'd trigger Ruff `E402` ("module-level import not at top of file")
when Phase 4 (ruff) of `prepare-python-recipe` runs.
- **Author-written bootstrap.** The recipe author already wrote
`load_dotenv()` + `os.environ.setdefault(...)` calls followed by a
trailing `from . import agent`, but never marked the trailing import.
The skill did NOT inject anything (load_dotenv was already present) but
still adds the noqa suffix so the file is lint-clean on the pipeline's
next ruff pass.
The suppression pass is precise — a relative import at the very TOP of
the file (before any non-import statement) is fine and left untouched.
Idempotent: a line that already carries `# noqa: E402` is skipped.
4. **Replaces hardcoded model names** in source (e.g. `model="gemini-3.5-flash"`
in `agent.py`) with **bare `os.getenv(...)`** — no default argument:
- Single model → `os.getenv("MODEL_NAME")`
- Multiple models → `os.getenv("MODEL_NAME_GENERATED_1")`, `os.getenv("MODEL_NAME_GENERATED_2")`, … (sorted alphabetically for determinism)
The actual model string is written as the value in `.env.example` (e.g.
`MODEL_NAME_GENERATED_1=gemini-3.5-flash`) with a comment prompting the
maintainer to rename the variable to something meaningful before shipping.
5. **Updates `pyproject.toml`** — adds `python-dotenv>=1.0.0` to `[project]`
dependencies if it is not already there.
---
## Hard rules the skill NEVER breaks
**Rule 1 — User-edit safety for `.env.example`.** The writer classifies
every existing entry before touching anything, and only rewrites lines
it can prove it authored. Concretely:
| Existing entry looks like | Source now provides | Action |
|---|---|---|
| Missing | any | **append** with resolved default (or TODO) |
| A line the skill wrote (has `# extracted-by:extract-env-vars` marker) with `<TODO: update-this-value>` value | a REAL default (not placeholder-shape) | **upgrade in place** |
| Same as above | no default OR a placeholder-shape default | **skip** (would be TODO → TODO churn) |
| Bare `VAR=<TODO: update-this-value>` line with **no marker AND no inline comment** — a v1-era TODO | a REAL default | **upgrade in place** (v1 → v2 migration) |
| A line the skill wrote with a REAL value | source has any value (even different) | **skip** (never overwrite skill-authored values on drift) |
| Anything else (user typed value, user-authored comment, non-standard formatting) | any | **skip — never touched** |
Golden rule underneath all of this: **fail closed**. If the classifier
can't confidently prove a line was skill-authored or is a bare v1 TODO,
it's `USER_OWNED` and untouchable. If the rewriter finds a line whose
structure it doesn't understand (quoted value, backslash continuation,
duplicate declarations), it refuses and warns rather than guessing. And
before any write, the assembled file is re-parsed to verify every
planned upgrade re-parses as a skill-authored real value — a bug in the
rewriter cannot silently corrupt `.env.example`.
To force regeneration of a line the skill would otherwise skip, delete
the line and re-run.
*Rationale for extracting defaults (v2) and upgrading stale TODOs
(v2.1): a value hidden inside a Python file that a user must know about
but has no reason to look at is, in practice, undocumented.
`.env.example` is the documented configuration surface. Surfacing what
the author already committed to in code — and updating a stale TODO
once source can supply a real default — makes the configuration
discoverable without inventing anything new. The value in source WAS
the value; we're only lifting it into view.*
**Rule 2 — Additive-only for Python files.** The skill never writes new
`os.environ.setdefault(...)` bootstrap lines into any Python file.
Pre-existing `os.environ.setdefault(...)` or
`os.getenv("VAR", "default")` calls that a recipe author wrote by hand
are LEFT UNTOUCHED. The skill's only writes to Python files are:
- Adding the `from dotenv import load_dotenv` + `load_dotenv()` snippet
(once, only if not already present).
- Appending `# noqa: E402` to trailing relative imports that would
otherwise trip Ruff after the env-bootstrap block.
- Replacing hardcoded model literals with bare `os.getenv(...)` calls.
Note that scanning `os.environ.setdefault(...)` and lifting its value
into `.env.example` (v2) does NOT violate Rule 2 — the skill READS from
Python source (unchanged behaviour) and WRITES only to `.env.example`.
---
## Rules for the Agent
1. **Always use the script** — never manually edit `.env.example`, `__init__.py`,
or `pyproject.toml` to perform these changes.
2. **Ask for the recipe directory** if the user has not provided one. Do not
assume a path.
3. **After the script succeeds**, remind the user to:
- Copy `.env.example` → `.env` and fill in real values before running locally.
- Run `uv sync` to pick up the `python-dotenv` dependency if it was newly added.
---
## Input
| Field | Required | Description |
|-------|----------|-------------|
| Recipe directory | Yes | Path to the recipe root (e.g. `contrib/my-recipe` or `core/python/my-recipe`) |
If the user has not specified the recipe directory, ask for it before proceeding.
---
## Run
Run it through `uv` so it always executes on a Python 3.11+ interpreter — the
script uses the stdlib `tomllib`, which only exists from 3.11 onward. A bare
`python3` that resolves to 3.9/3.10 fails with `ModuleNotFoundError: tomllib`.
No `--with` packages are needed (the script is stdlib-only).
```bash
uv run --no-project python3 \
.agents/skills/extract-python-environment-variables/scripts/extract_env_vars.py \
--recipe-dir <RECIPE_DIR>
```
### Preview first (optional)
Add `--dry-run` to report exactly what *would* change without modifying any
files. Nothing is written to `.env.example`, `__init__.py`, `pyproject.toml`, or
any source file. Useful for inspecting a recipe before committing to the edits:
```bash
uv run --no-project python3 \
.agents/skills/extract-python-environment-variables/scripts/extract_env_vars.py \
--recipe-dir <RECIPE_DIR> --dry-run
```
In dry-run output, actions are prefixed with `[DRY-RUN]` and phrased as
"Would add" / "Would inject" / "Would replace".
---
## Respond
Do not show the script's raw stdout. Reformat its results into clear Markdown
tables (variables to add, files to update, model replacements) so they are easy
to read — this is especially important for `--dry-run` output. For each variable
or model string, include a column with the source file where it was found
(locate it in the recipe's Python source; ignore `.env` and `.env.example`).
For added variables, surface the **resolved default** and **its provenance**
so the maintainer knows which value the skill chose and where it came from:
| Variable | Value written to .env.example | Source |
|----------|-------------------------------|--------|
| `GOOGLE_CLOUD_PROJECT` | `<TODO: update-this-value>` *(downgraded — source had `"my-project-id"`)* | `agent.py` (setdefault) |
| `GOOGLE_GENAI_USE_VERTEXAI` | `TRUE` | `__init__.py` (setdefault) |
| `LOG_LEVEL` | `INFO` | `logging_setup.py` (getenv fallback) |
| `API_KEY` | `<TODO: update-this-value>` *(no default in source)* | `client.py` (getenv, no fallback) |
If the script reports **upgrades** (stale TODO entries rewritten in
place with real defaults from source), surface those in a separate table
so the maintainer can eyeball each one:
| Variable | Was | Now | Source |
|----------|-----|-----|--------|
| `USE_VERTEX` | `<TODO: update-this-value>` | `TRUE` | `__init__.py` (setdefault) |
If the script logged `[WARN] Refused to upgrade …` for any variable,
surface that too — the line's structure was ambiguous and was left
untouched deliberately.
Once the script finishes successfully, summarise what changed:
- Which variables were **added** to `.env.example` (with resolved value +
source) or confirm it was already up to date.
- Which existing TODOs were **upgraded** in place (v2.1) with real
defaults from source.
- Any values that were **downgraded to `<TODO>` because they looked like
placeholders** — the maintainer should fix the source too.
- Whether `load_dotenv()` was injected or was already present.
- Whether `python-dotenv` was added to `pyproject.toml` or was already there.
Then remind the user of the next steps:
```
Next steps:
cp <RECIPE_DIR>/.env.example <RECIPE_DIR>/.env # then fill in real values
cd <RECIPE_DIR> && uv sync # install python-dotenv if newly added
```
Do not make any further changes. End your turn.
---
**Source:** [`google/adk-samples`](https://github.com/google/adk-samples) → `.agents/skills/extract-python-environment-variables/SKILL.md`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!