Typed access for your language
Generate a typed env file from your .env.schema in TypeScript, Python, Go, Rust, Java, PHP or C#, keep its settings in .penv/config.toml, and check it in CI.
penv gen # list targets: source, output file, options, detected directories
penv gen ts
penv gen go --out internal/env/env.gopenv gen writes generated code into your repository; your app imports nothing from penv at runtime. Data types come from @env-spec; this page covers only how penv renders them.
Targets
| Target | Detected by | Import line penv prints | Compile check |
|---|---|---|---|
ts | package.json, tsconfig.json | import { env } from "<path>" | tsc --strict |
py | pyproject.toml, requirements.txt, setup.py, Pipfile | from <module> import env | py_compile |
go | go.mod | import "<module>/env" | gofmt -l -e |
rust | Cargo.toml | mod env; use env::Env; | none |
java | pom.xml, build.gradle[.kts] | import config.Env; | none |
php | composer.json | use App\Env; | php -l |
csharp | *.csproj, *.sln | using App; | none |
Detection searches three levels deep, skipping node_modules, dist, build, target and dot-directories. penv edits no build config; you paste the import line. The ts line follows tsconfig.json extends and uses a paths alias when one reaches the file.
Type mapping
| Base type | ts | py | go | rust | java | php | csharp |
|---|---|---|---|---|---|---|---|
string, url, email | string | str | string | String | String | string | string |
number | number | float | float64 | f64 | double | float | double |
number(isInt=true) | number | int | int64 | i64 | long | int | long |
port | number | int | int | u16 | int | int | int |
boolean | boolean | bool | bool | bool | boolean | bool | bool |
enum(a,b) | "a" | "b" | Literal[...] | string | String | String | string | string |
An optional key with no default is T | undefined (ts), T | None (py), a pointer (go) or Option<T> (rust).
Secrets in generated code
| Target | Sensitive string, url or email key |
|---|---|
go, rust, java, php, csharp | Type Secret, which prints [redacted]. Value(), expose() or Expose() returns it |
ts | Read by computed name, so no bundler inlines it. With mask = true (default), console output and Response bodies are masked in Node, Bun, Deno, edge and Workers |
py | str; SecretStr with pydantic = true |
ts exports one env for server and client code. A @public key is read by its literal name so your bundler can inline it. The other targets expose Env.load() (Load() in go and C#), whose error names each problem and never a value.
Output path
Only --out or output in [targets.<name>] sets the path. At a terminal with neither, penv lists detected directories:
# PATH
1 apps/web/src/env.ts
2 apps/api/src/env.ts
where should env.ts go? [apps/web/src/env.ts] (Enter, number, path, none):Enter takes the first suggestion, a number picks one, a path is used relative to your repository root, and none skips the target. A path outside the repository is refused with output_outside_repo.
With no terminal and no --out, penv writes nothing for that target, reports it skipped, and exits 3. Pass --out <PATH> in your scripts and pipelines.
Settings
[targets.ts]
output = "apps/web/src/env.ts"
[targets.ts.options]
key_case = "camel"
mask = true
runtime = "node"penv writes the section after every answer, defaults included, and never asks again. Commit .penv/config.toml. A .penv/targets/<name>/ folder from an earlier release migrates on the next gen: fields into the section, env.tmpl to .penv/<name>.tmpl.
| Target | Option | Values | Default |
|---|---|---|---|
ts | key_case | upper, camel | upper |
ts | runtime | node, vite, deno, workers | node; suggested from vite.config.*, wrangler.*, deno.json |
ts | mask | true, false | true |
py | pydantic | false, true | false; suggested when pyproject.toml lists pydantic |
go, java | package | any | env, config |
php, csharp | namespace | any | App |
penv gen <target> --options prints them. penv prompts only when a suggestion differs from the default; with no terminal it takes the default.
Custom target
[targets.<name>] plus .penv/<name>.tmpl overrides a built-in target or adds a language. Lookup order: your repository, then ~/.penv/targets/<name>/ (target.toml, env.tmpl), then built-in. A location missing a file inherits it from the next. A template receives only penv schema --json: no values, no network.
Procedure
Run penv gen <target> and choose the output path.
Paste the printed import line into your settings module.
Commit .penv/config.toml with the generated file.
Add penv gen <target> --check to your CI. It diffs against the file on disk, runs the compile check when the tool is installed, never prompts, and exits 0 for a skipped target.