<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: penv</title>
    <description>The latest articles on DEV Community by penv (@penvhq-updates).</description>
    <link>https://dev.to/penvhq-updates</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119548%2F79de780b-39c8-4b06-8697-d5dac0cd0448.jpg</url>
      <title>DEV Community: penv</title>
      <link>https://dev.to/penvhq-updates</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/penvhq-updates"/>
    <language>en</language>
    <item>
      <title>Why penv adopted @env-spec instead of inventing a schema format</title>
      <dc:creator>penv</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:01:00 +0000</pubDate>
      <link>https://dev.to/penvhq-updates/why-penv-adopted-env-spec-instead-of-inventing-a-schema-format-5hb9</link>
      <guid>https://dev.to/penvhq-updates/why-penv-adopted-env-spec-instead-of-inventing-a-schema-format-5hb9</guid>
      <description>&lt;p&gt;Every tool that touches &lt;code&gt;.env&lt;/code&gt; eventually wants a schema. You need to know that &lt;code&gt;DATABASE_URL&lt;/code&gt; is a URL and &lt;code&gt;PORT&lt;/code&gt; is a number, which keys are required, and, if you care about coding agents at all, which values must never be echoed. The obvious move is to design a little format for that. We nearly did.&lt;/p&gt;

&lt;p&gt;Then we read &lt;code&gt;@env-spec&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What @env-spec is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@env-spec&lt;/code&gt; is the decorator vocabulary that &lt;a href="https://varlock.dev" rel="noopener noreferrer"&gt;varlock&lt;/a&gt; introduced: comments above a key in a dotenv-shaped file, carrying metadata the loader can act on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# @type=url&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="c"&gt;# @type=port @sensitive=false&lt;/span&gt;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole idea. The file still parses as dotenv. Every editor already highlights it. A person who has never seen the spec reads it correctly on the first try, and a person who has seen it reads it instantly.&lt;/p&gt;

&lt;p&gt;We did not think we could improve on that, so we didn't try. penv's &lt;code&gt;.env.schema&lt;/code&gt; is an &lt;code&gt;@env-spec&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that bought us
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zero learning curve for the committed file.&lt;/strong&gt; The thing penv puts in your repo is the thing people most need to trust and edit. Making it a format with an existing spec and an existing community meant we never had to write "how to read a .env.schema" docs. The decorators mean what the spec says they mean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A guessable &lt;code&gt;init&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;penv init&lt;/code&gt; reads the &lt;code&gt;.env&lt;/code&gt; you already have, writes the schema next to it, and gitignores &lt;code&gt;.env&lt;/code&gt;. Because the output is dotenv-shaped, the guesses it makes are visible and correctable in the same file: if it marks a key sensitive that isn't, you change one decorator. Every key is sensitive and required unless a bundler prefix (&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, &lt;code&gt;VITE_&lt;/code&gt;, &lt;code&gt;EXPO_PUBLIC_&lt;/code&gt; and friends) or a dull value (&lt;code&gt;3000&lt;/code&gt;, &lt;code&gt;us-east-1&lt;/code&gt;) says otherwise, and a key &lt;em&gt;named&lt;/em&gt; like a secret (&lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;, &lt;code&gt;..._ANON_KEY&lt;/code&gt;) stays sensitive whatever it holds and whatever prefix it carries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interoperability we didn't have to negotiate.&lt;/strong&gt; A team that already uses varlock can drop the same file into penv. A team that leaves penv walks away with a file another tool understands. That last part matters to us more than it sounds; one of the principles in our design doc is "drop-in or evict", and a proprietary schema format would have quietly broken the evict half.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we added
&lt;/h2&gt;

&lt;p&gt;penv needs a few things the base vocabulary does not carry, so it reads three extra decorators, &lt;code&gt;@since&lt;/code&gt;, &lt;code&gt;@rotate&lt;/code&gt; and &lt;code&gt;@dynamicFrom&lt;/code&gt;, plus two headers. &lt;code&gt;@schema=1&lt;/code&gt; names the grammar version the file was written in. &lt;code&gt;@penv=&amp;lt;org&amp;gt;/&amp;lt;project&amp;gt;&lt;/code&gt; names the cloud project the schema belongs to; leave it out and penv runs in local mode, and nothing leaves your machine.&lt;/p&gt;

&lt;p&gt;They follow the same shape as the spec's own decorators on purpose. A varlock user seeing &lt;code&gt;@rotate&lt;/code&gt; for the first time will not know exactly what penv does with it, but will know exactly where it goes and how to read it. We'll document their semantics fully with the release candidate rather than half-document them now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing we left out, on purpose
&lt;/h2&gt;

&lt;p&gt;This came up publicly this week, from the people who wrote the spec, and it deserves a straight answer.&lt;/p&gt;

&lt;p&gt;varlock can set values in the schema and load a cascade of env files: &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.production&lt;/code&gt;, and so on, layered in a defined order. It is a useful model and a lot of teams rely on it.&lt;/p&gt;

&lt;p&gt;penv does not do the cascade. It reads exactly one local file, &lt;code&gt;.env&lt;/code&gt;, from the directory that holds the schema, and no &lt;code&gt;.env.*&lt;/code&gt; variant is ever layered on top. There is one committed schema, one local &lt;code&gt;.env&lt;/code&gt; (which &lt;code&gt;penv init&lt;/code&gt; gitignores), and, once you push, environments live in the cloud, addressed as &lt;code&gt;org/project/environment&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reason is the problem penv was built for. A coding agent runs as you, and the deny rules people write by hand for their harnesses are almost always written against &lt;code&gt;.env&lt;/code&gt;, singular. &lt;code&gt;.env.local&lt;/code&gt; and &lt;code&gt;.env.production&lt;/code&gt; are precisely the files those rules miss, and precisely the files that hold the interesting values. penv's own guards deny &lt;code&gt;.env.*&lt;/code&gt; as a pattern across eight harnesses, so the cost is not enforcement. The cost is surface: every extra file in a cascade is another place a value can live and another file a grep for context can surface. We would rather have one file to keep out of reach than a well-ordered set of five.&lt;/p&gt;

&lt;p&gt;So: non-sensitive values do live in the schema (&lt;code&gt;PORT=3000&lt;/code&gt; and &lt;code&gt;DEBUG=true&lt;/code&gt; go straight in during &lt;code&gt;init&lt;/code&gt;). Anything that could be a secret never does. And the layering that varlock does well on a laptop, penv moves to the server, where a value can be short-lived, scoped and attributed to the session that used it.&lt;/p&gt;

&lt;p&gt;That is a real trade. It makes penv less flexible on a single machine than varlock is. If a team's workflow is built around the cascade, varlock is the better fit, and we would say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use varlock, then
&lt;/h2&gt;

&lt;p&gt;Because they are solving adjacent problems, not the same one. varlock is the schema and the local loader, done well. penv is downstream of that: the per-harness guards, the output masking under an agent session, the typed codegen, and the path to a shared store when there is more than one laptop. We adopted the spec so that those layers sit on a format that isn't ours, and so that nobody has to choose a schema dialect in order to choose a tool.&lt;/p&gt;

&lt;p&gt;If you're building something that touches &lt;code&gt;.env&lt;/code&gt;, our unsolicited advice is the same thing we did: read &lt;code&gt;@env-spec&lt;/code&gt; before designing a format. The spec is small, it's readable, and the people behind it clearly intended for others to build on it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;penv: &lt;a href="https://github.com/penvhq/penvhq" rel="noopener noreferrer"&gt;https://github.com/penvhq/penvhq&lt;/a&gt; (MIT, &lt;code&gt;1.0.0-alpha.3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;@env-spec and varlock: &lt;a href="https://varlock.dev" rel="noopener noreferrer"&gt;https://varlock.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: penv and varlock are separate projects. We have no affiliation beyond having adopted their spec and having argued with them, politely, on X.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devops</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Your coding agent can read your .env. Here's what we did about it.</title>
      <dc:creator>penv</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:41:20 +0000</pubDate>
      <link>https://dev.to/penvhq-updates/your-coding-agent-can-read-your-env-heres-what-we-did-about-it-5go9</link>
      <guid>https://dev.to/penvhq-updates/your-coding-agent-can-read-your-env-heres-what-we-did-about-it-5go9</guid>
      <description>&lt;p&gt;Run &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;, &lt;code&gt;amp&lt;/code&gt;, whatever. It starts a shell as your user. Your user can read &lt;code&gt;.env&lt;/code&gt;. So the agent can read &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the whole problem. Not a bug in any harness, not a jailbreak, just the permission model working as designed. And it means that whenever an agent greps around your repo for context, or a tool call echoes the environment, or a stack trace lands in a transcript, your Stripe key can go with it.&lt;/p&gt;

&lt;p&gt;Most harnesses have a deny list you can configure. Almost nobody does, the syntax differs per tool, and a deny rule on &lt;code&gt;.env&lt;/code&gt; does nothing about &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.production&lt;/code&gt;, or the fact that &lt;code&gt;printenv&lt;/code&gt; in a hook still works.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://github.com/Itzfeminisce/penvhq" rel="noopener noreferrer"&gt;penv&lt;/a&gt; because we were tired of hand-writing those configs and then not trusting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What penv actually does
&lt;/h2&gt;

&lt;p&gt;One static Rust binary. No runtime, no daemon, no account required for the first minute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://penv.cloud/install | sh   &lt;span class="c"&gt;# or: npm i -g @penvhq/cli&lt;/span&gt;
penv init                                    &lt;span class="c"&gt;# reads .env, writes .env.schema, gitignores .env&lt;/span&gt;
penv run &lt;span class="nt"&gt;--&lt;/span&gt; pnpm dev                         &lt;span class="c"&gt;# validates, injects, masks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; writes exactly one file into your repo, &lt;code&gt;.env.schema&lt;/code&gt;. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# @schema=1&lt;/span&gt;

&lt;span class="c"&gt;# @type=url&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="c"&gt;# @type=string&lt;/span&gt;
&lt;span class="nv"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="c"&gt;# @type=port @sensitive=false&lt;/span&gt;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No values that could be secrets are ever copied in. Every key is sensitive and required by default; a bundler prefix like &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; or an obviously dull value like &lt;code&gt;3000&lt;/code&gt; flips that, and a key &lt;em&gt;named&lt;/em&gt; like a secret (&lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;, &lt;code&gt;..._ANON_KEY&lt;/code&gt;) stays sensitive no matter what it holds. The decorators follow the &lt;code&gt;@env-spec&lt;/code&gt; vocabulary, so if you have used varlock you can already read it.&lt;/p&gt;

&lt;p&gt;The schema is committed. &lt;code&gt;.env&lt;/code&gt; is not. That split is what makes the rest possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that gives you against an agent
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Validation before exec.&lt;/strong&gt; &lt;code&gt;penv run&lt;/code&gt; checks every value against the schema before your process starts. A missing or malformed key fails with exit code 3, not a runtime error forty seconds in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Masking.&lt;/strong&gt; When penv detects an agent session (it reads the harness env vars first, then walks process ancestry), it scrubs every sensitive value from the child's stdout and stderr — raw, hex, base64, and URL-encoded forms. The agent sees &lt;code&gt;***&lt;/code&gt;, not the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Harness guards, generated.&lt;/strong&gt; &lt;code&gt;penv guard&lt;/code&gt; writes what each harness actually enforces, from the schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deny rules and a sandbox block for Claude Code&lt;/li&gt;
&lt;li&gt;a permission profile for Codex&lt;/li&gt;
&lt;li&gt;deny rules and fail-closed hooks for Cursor&lt;/li&gt;
&lt;li&gt;the equivalents for Copilot CLI, Gemini, Cline, Windsurf, and Amp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hook is the penv binary itself, never a shell script that fails open when something goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typed access.&lt;/strong&gt; &lt;code&gt;penv gen ts&lt;/code&gt; or &lt;code&gt;penv gen py&lt;/code&gt; writes a typed accessor for your language so code stops doing &lt;code&gt;process.env.FOO!&lt;/code&gt; and reads through a validated object. Targets are folders with a template; adding a language is adding a folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim, and only the claim
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;penv guard --check&lt;/code&gt; prints one sentence, and we keep it exactly true:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;penv keeps secrets out of the files, the repo, the shell history and the captured output an agent reads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the layer where leaks actually happen — a grep, a transcript, a pasted stack trace — and it is the layer penv closes. penv is not a sandbox and does not claim to be one; the process you launch still runs as you. So the cloud side is built for that: every value it issues is short-lived, scoped to the session, and attributable to it. A value that escapes is already expired and already traced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cloud is the upgrade, not the product
&lt;/h2&gt;

&lt;p&gt;Everything above works with zero account. When you have a team:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv login   &lt;span class="c"&gt;# device code in the browser&lt;/span&gt;
penv push    &lt;span class="c"&gt;# values go to penv.cloud, .env is deleted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there &lt;code&gt;.env&lt;/code&gt; is a view you can regenerate, not the source of truth. A teammate clones and runs &lt;code&gt;penv run -- pnpm dev&lt;/code&gt;; that is the onboarding. CI presents its OIDC token and gets a fifteen-minute credential. A server with nothing to present enrols a keypair once. &lt;code&gt;penv reveal KEY&lt;/code&gt; under an agent session needs a human to click approve in the console — the agent can ask, it cannot self-serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status, honestly
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@penvhq/cli&lt;/code&gt; is at &lt;code&gt;1.0.0-alpha.3&lt;/code&gt;. It is published, signed, builds for macOS/Linux/Windows on x86_64 and arm64, and we use it daily. We do not expect further breaking changes before RC, but it is an alpha. Local mode needs nothing from us.&lt;/p&gt;

&lt;p&gt;penv.cloud is in private beta. We are letting early users in from the &lt;a href="https://penv.cloud" rel="noopener noreferrer"&gt;waitlist&lt;/a&gt; in small batches, and the teams we onboard during the beta keep enterprise-tier benefits at launch — if you want your team on it, sign up.&lt;/p&gt;

&lt;p&gt;If you run a coding agent against a repo with a &lt;code&gt;.env&lt;/code&gt; in it — and you almost certainly do — try &lt;code&gt;penv init&lt;/code&gt; on it and read the schema it writes. If it guesses a key wrong, that is a bug report we want.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/Itzfeminisce/penvhq" rel="noopener noreferrer"&gt;https://github.com/Itzfeminisce/penvhq&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install + waitlist: &lt;a href="https://penv.cloud" rel="noopener noreferrer"&gt;https://penv.cloud&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@penvhq/cli" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@penvhq/cli&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>rust</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
