env_example
Turn a .env file given inline into a .env.example: every value blanked or replaced by a type hint (<url>, <number>, <boolean>, <email>, <host>, <path>, <secret>, <interpolated>, <string>), with comments, blank lines, the export prefix and the quoting kept. Flags duplicate keys, secret-looking keys that carry a value, values that look like AWS, GitHub, Slack, Stripe or Google keys, JWTs, PEM private keys or URLs with a password, and commented-out assignments. No input value is ever printed.
Install
/tools install env_example /tools install env_example@1.0.0
In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · env_example 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool env_example; each run is approved as a local_exec.
Arguments
One JSON object on stdin, validated against the schema before the program starts.
| name | type | required | default | meaning |
|---|---|---|---|---|
text | string | yes | The .env content. length ≤ 500000 | |
hints | boolean | no | true | true: a type hint in place of each value; false: empty values |
Input schema (JSON)
{
"type": "object",
"required": [
"text"
],
"additionalProperties": false,
"properties": {
"text": {
"type": "string",
"maxLength": 500000,
"description": "The .env content"
},
"hints": {
"type": "boolean",
"default": true,
"description": "true: a type hint in place of each value; false: empty values"
}
}
}
Example
Input
{
"text": "# App\nexport PORT=8080\nDATABASE_URL=\"postgres://admin:hunter2@db:5432/shop\"\nDEBUG=true\nPORT=9090\n"
}
Output (stdout)
{
"example": "# App\nexport PORT=<number>\nDATABASE_URL=\"<secret>\"\nDEBUG=<boolean>\nPORT=<number>\n",
"duplicates": [
{
"key": "PORT",
"lines": [
2,
5
],
"note": "most loaders keep the last one"
}
]
}
Files
The command the install card registers is python3 env_example.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.
env_example.py · sha256 ad7f4a8b1d32073da1043b9edb185aab20d36da5dda32bb1ee0dca1eb1313923
#!/usr/bin/env python3
"""Write a .env.example from a .env text: every value blanked or replaced by a type hint
(<url>, <number>, <secret>...), comments, blank lines, `export` and quoting kept, duplicate
keys and secret-looking entries flagged. No value of the input is ever printed.
Input (JSON on stdin):
text the .env content
hints true (default): replace each value by a type hint; false: leave it empty
"""
import json
import math
import re
import sys
MAX_TEXT = 500_000
ASSIGN = re.compile(r"^(?P<indent>\s*)(?P<export>export\s+)?(?P<key>[A-Za-z_][A-Za-z0-9_.\-]*)(?P<eq>\s*=\s*)(?P<rest>.*)$")
COMMENTED = re.compile(r"^(?P<hash>\s*#+\s*)(?P<export>export\s+)?(?P<key>[A-Za-z_][A-Za-z0-9_]*)(?P<eq>\s*=\s*)(?P<rest>.*)$")
SECRET_KEY = re.compile(r"(SECRET|PASSW(OR)?D|\bPWD\b|_PWD$|^PWD|TOKEN|API_?KEY|ACCESS_?KEY|PRIVATE|CREDENTIAL|"
r"AUTH|SIGNING|ENCRYPTION|SALT|PASSPHRASE|_KEY$|^KEY$|DSN$|WEBHOOK)", re.I)
TOKEN_PATTERNS = [
("an AWS access key id", re.compile(r"\b(AKIA|ASIA)[0-9A-Z]{16}\b")),
("a GitHub token", re.compile(r"\b(gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,})")),
("a Slack token", re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}")),
("an OpenAI/Anthropic-style API key", re.compile(r"\bsk-(ant-)?[A-Za-z0-9_\-]{20,}")),
("a Stripe live key", re.compile(r"\b[sr]k_live_[A-Za-z0-9]{16,}")),
("a Google API key", re.compile(r"\bAIza[0-9A-Za-z_\-]{35}\b")),
("a JSON Web Token", re.compile(r"\beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}")),
("a PEM private key", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
]
URL_CRED = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*://[^/\s:@]+:[^/\s@]+@")
def fail(message):
print(json.dumps({"error": message}))
sys.exit(2)
def entropy(value):
counts = {}
for ch in value:
counts[ch] = counts.get(ch, 0) + 1
return -sum(c / len(value) * math.log2(c / len(value)) for c in counts.values())
def split_value(rest):
"""Return (quote, value, tail, closed) for the text after '='. tail is an inline comment."""
if rest[:1] in ("'", '"', "`"):
q = rest[0]
i = 1
while i < len(rest):
if rest[i] == "\\" and q == '"':
i += 2
continue
if rest[i] == q:
return q, rest[1:i], rest[i + 1:], True
i += 1
return q, rest[1:], "", False
m = re.search(r"\s+#", rest)
if m:
return "", rest[:m.start()], rest[m.start():], True
return "", rest.rstrip(), "", True
def kind(key, value):
"""A hint and the flags for one value; the value itself is never returned."""
flags = []
v = value.strip()
for label, rx in TOKEN_PATTERNS:
if rx.search(v):
flags.append(f"value looks like {label}")
if URL_CRED.match(v):
flags.append("URL with an embedded user and password")
secret_name = bool(SECRET_KEY.search(key))
if not v:
return "", flags
if not flags and ("${" in v or re.search(r"(^|[^\\])\$[A-Za-z_]", v)):
return "<interpolated>", flags
if secret_name:
flags.append("secret-looking key with a value (rotate it if this file was ever shared or committed)")
if flags or secret_name:
return "<secret>", flags
if re.fullmatch(r"[A-Za-z][A-Za-z0-9+.\-]*://\S+", v):
return "<url>", flags
if re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", v):
return "<number>", flags
if v.lower() in ("true", "false", "yes", "no", "on", "off"):
return "<boolean>", flags
if re.fullmatch(r"[^@\s]+@[^@\s]+\.[A-Za-z]{2,}", v):
return "<email>", flags
if re.fullmatch(r"(\d{1,3}\.){3}\d{1,3}(:\d+)?|[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+:\d+|localhost(:\d+)?", v):
return "<host>", flags
if re.fullmatch(r"(\.{0,2}/|~/)\S*", v):
return "<path>", flags
if len(v) >= 24 and " " not in v and entropy(v) >= 4.0:
flags.append("long random-looking value")
return "<secret>", flags
return "<string>", flags
def main():
try:
request = json.load(sys.stdin)
except ValueError as error:
fail(f"stdin is not JSON: {error}")
if not isinstance(request, dict):
fail("stdin must be a JSON object")
extra = sorted(set(request) - {"text", "hints"})
if extra:
fail(f"unknown field(s): {', '.join(extra)}")
text = request.get("text")
if not isinstance(text, str):
fail("text must be a string with the .env content")
if len(text) > MAX_TEXT:
fail(f"text is longer than {MAX_TEXT} characters")
hints = request.get("hints", True)
if not isinstance(hints, bool):
fail("hints must be true or false")
lines = text.splitlines()
out, variables, notes = [], [], []
seen = {}
i = 0
while i < len(lines):
number, line = i + 1, lines[i]
i += 1
stripped = line.strip()
if not stripped:
out.append(line)
continue
m = ASSIGN.match(line)
commented = False
if not m and stripped.startswith("#"):
m = COMMENTED.match(line)
if not m:
out.append(line)
continue
commented = True
if not m:
out.append(f"# (line {number} removed: not a KEY=value assignment)")
notes.append({"line": number, "issue": "not a KEY=value assignment; removed so no value can leak"})
continue
key = m.group("key")
quote, value, tail, closed = split_value(m.group("rest"))
flags = []
if quote and not closed:
# a multi-line quoted value: consume until the closing quote
parts = [value]
while i < len(lines):
nxt = lines[i]
i += 1
end = nxt.find(quote)
if end >= 0:
parts.append(nxt[:end])
tail = nxt[end + 1:]
closed = True
break
parts.append(nxt)
value = "\n".join(parts)
flags.append("multi-line value" if closed else "unterminated quote: the rest of the file was read as its value")
if tail and not re.match(r"\s*(#.*)?$", tail):
flags.append("text after the closing quote was dropped")
tail = ""
hint, value_flags = kind(key, value)
flags += value_flags
placeholder = hint if hints else ""
lead = m.group("hash") if commented else m.group("indent")
export = m.group("export") or ""
out.append(f"{lead}{export}{key}{m.group('eq').rstrip() if not placeholder and not quote else m.group('eq')}"
f"{quote}{placeholder}{quote}{tail}")
if commented:
flags.append("commented-out assignment: its value was removed too")
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
flags.append("key is not a portable shell variable name")
entry = {"key": key, "line": number, "hint": hint or "<empty>", "export": bool(export),
"quote": {"'": "single", '"': "double", "`": "backtick"}.get(quote, "none"), "commented": commented}
if flags:
entry["flags"] = flags
variables.append(entry)
if not commented:
seen.setdefault(key, []).append(number)
duplicates = [{"key": k, "lines": v, "note": "most loaders keep the last one"} for k, v in seen.items() if len(v) > 1]
suspicious = [{"key": e["key"], "line": e["line"], "flags": [f for f in e["flags"] if "secret" in f or "looks like" in f
or "password" in f or "random" in f]}
for e in variables if e.get("flags")]
suspicious = [s for s in suspicious if s["flags"]]
example = "\n".join(out) + ("\n" if text.endswith(("\n", "\r")) or not out else "")
result = {"example": example, "variables": variables, "count": len([v for v in variables if not v["commented"]]),
"duplicates": duplicates, "suspicious": suspicious, "mode": "hints" if hints else "blank"}
if notes:
result["removed_lines"] = notes
print(json.dumps(result))
if __name__ == "__main__":
main()
Where it is served
The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/env_example/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/env_example/1.0.0/env_example.py