Autonoma detects hardcoded secrets and replaces them with os.environ[...] references using AST rewrites. Changes are applied only when the rewrite is deterministic and semantically-preserving.
Rewrites use the parsed syntax tree — not regex on raw text. Runs locally with no network calls. Works in CI with idempotent, minimal diffs.
What problem this solves
Hardcoded secrets in codebases:
- secrets get committed and stay in git history
- fixing them manually breaks code or misses edge cases
- teams detect leaks but avoid auto-fix tools because they are unsafe
Most tools detect them.
Autonoma only touches what it can prove is safe to change.
Quick example
autonoma scan . autonoma fix . git diff
Installation
Pre-commit Integration
Add this to your .pre-commit-config.yaml to prevent secrets from entering your history:
- repo: local hooks: - id: autonoma name: Autonoma Scan entry: autonoma scan language: system types: [python]
Try it in 60 seconds
# 1. Create a test file with hardcoded secrets cat > test_secrets.py << 'EOF' SENDGRID_API_KEY = "sg-live-abc123xyz789" DB_PASSWORD = "Pr0dAccess2024!" EOF # Also create the env contract file (required for safe remediation) printf 'SENDGRID_API_KEY=\nDB_PASSWORD=\n' > .env.example # 2. Scan — emits JSON findings to stdout autonoma scan test_secrets.py # 3. Fix — rewrites the file in place autonoma fix test_secrets.py # 4. Scan again — should now be clean (exit 0) autonoma scan test_secrets.py # 5. Inspect the result cat test_secrets.py
Expected output after fix:
import os SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"] DB_PASSWORD = os.environ["DB_PASSWORD"]
Commands
scan
Detection mode. Outputs JSON to stdout and a human-readable summary to stderr. Non-mutating — never modifies files.
# Scan a directory (JSON findings to stdout) autonoma scan src/ # Save JSON results to a file autonoma scan src/ > findings.json
Exit codes for scan:
| Code | Meaning |
|---|---|
0 |
No findings |
1 |
Findings detected |
3 |
Tool error |
fix
Remediates hardcoded secrets using AST rewrites. Mutates files in place.
# Apply fixes autonoma fix src/ # Preview patches before writing autonoma fix src/ --diff # Write remediation audit log autonoma fix src/ --report-out audit.json
Exit codes for fix:
| Code | Meaning |
|---|---|
0 |
No findings — repo was already clean |
1 |
Findings existed before remediation (remediation may have succeeded — check output for FIXED/REFUSED counts) |
3 |
Tool error |
The
fixcommand exits1whenever it found secrets before attempting remediation, regardless of whether the rewrite succeeded. This is intentional: CI pipelines should flag the commit where secrets were introduced, even after auto-fix. Runautonoma scanafterward to confirm the repo is clean.
history-scan
Scans git history for secrets that were committed and later removed or changed.
Note
Detection only. This command does not rewrite git history or modify commits.
Before / After
These are the patterns Autonoma actually fixes today.
Before
# settings.py SENDGRID_API_KEY = "sg-live-abc123xyz789" DB_PASSWORD = "Pr0dAccess2024!"
After (autonoma fix .)
# settings.py import os SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"] DB_PASSWORD = os.environ["DB_PASSWORD"]
Refused (refusal-first safety)
# f-string — refused because the rewrite would change semantics api_key = f"prefix_{BASE_KEY}" # → REFUSED: refuse_fstring_mixed_expression # Dict/nested value — refused because the target is not a simple assignment DATABASES = { "default": {"PASSWORD": "Pr0d@ccess2024!"} } # → REFUSED: unsupported assignment target type
Refused findings are reported in the JSON output and cause a non-zero exit in CI. Files with refused findings are never modified.
What Autonoma fixes vs refuses
| Pattern | Example | Behavior | Why |
|---|---|---|---|
| Simple assignment | api_key = "sk-abc123" |
Fixed | Deterministic AST rewrite |
| Class attribute | class C: SECRET = "abc" |
Fixed | Deterministic AST rewrite |
| Keyword argument | connect(password="abc") |
Fixed | Deterministic AST rewrite |
| f-string | key = f"prefix_{v}" |
Refused | Rewrite would change runtime behavior |
| Concatenation | key = "sk-" + suffix |
Refused | Rewrite would change runtime behavior |
| Dict/nested value | cfg = {"pass": "abc"} |
Refused | Not a simple assignment target |
| Multiple assignment | A = B = "secret" |
Refused | Ambiguous target |
| Already safe | key = os.getenv("KEY") |
Skipped | No change needed |
Missing .env.example |
any pattern | Refused | No env contract to derive variable name |
CI/CD Features
Re-running on a clean file makes no changes. Rewrites keep original indentation and comments. import os is added only if it's missing.
Integration & CI/CD
GitHub Actions (Scan Only)
To fail your build if any secrets are detected:
- name: Scan for secrets run: autonoma scan .
Legacy Commands
analyze is retained for backwards compatibility. Migrate to scan or fix.
# Equivalent to 'autonoma scan' autonoma analyze src/ --detect-only # Equivalent to 'autonoma fix' autonoma analyze src/ --auto-fix
Constraints & Behaviors
What it remediates
- Simple assignments:
API_KEY = "secret" - Class attributes:
class Config: PASS = "secret" - Keyword arguments:
connect(password="secret")
What it refuses (by design)
- Complex Expressions: f-strings, concatenations, or function calls on the RHS.
- Ambiguous Targets: Multiple assignments (
A = B = "secret") or tuple unpacking. - Nested/Dict Values: Values inside dicts, lists, or tuples.
- Missing Context: If no
.env.exampleor environment contract is found in the repo.
Refused cases are reported in JSON output and will cause non-zero exit codes in CI. The file is never modified if any issue in it is refused.
What it does not do
- It does not use entropy/guessing (it uses heuristic name matching).
- It does not modify non-Python files in the Community Edition.
- It does not delete your code; backups are written as
<file>.bakbefore modification.
JSON Schema
autonoma scan outputs a detect-only report to stdout:
{
"schema_version": "1.0",
"tool_name": "autonoma",
"tool_version": "0.1.5",
"generated_at": "2026-03-24T12:00:00Z",
"mode": "detect-only",
"summary": {
"files_processed": 3,
"total_findings": 2,
"safe_to_fix": 1,
"refused": 1
},
"findings": [
{
"file": "settings.py",
"line": 4,
"pattern_type": "api_key",
"severity": "high",
"rule_id": "SEC002",
"safe_to_fix": true,
"suggested_env_var": "SENDGRID_API_KEY",
"refusal_reason": null,
"fingerprint": "sha256:abc123..."
}
]
}License
MIT License
