<?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: Oluwafemi</title>
    <description>The latest articles on DEV Community by Oluwafemi (@nightlyhermes).</description>
    <link>https://dev.to/nightlyhermes</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%2F4126325%2F31fed8be-9207-4747-af7b-741f3a785d52.png</url>
      <title>DEV Community: Oluwafemi</title>
      <link>https://dev.to/nightlyhermes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nightlyhermes"/>
    <language>en</language>
    <item>
      <title>Same logs, no secrets: penv 1.0.0-rc.3</title>
      <dc:creator>Oluwafemi</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:30:47 +0000</pubDate>
      <link>https://dev.to/nightlyhermes/same-logs-no-secrets-penv-100-rc3-38ko</link>
      <guid>https://dev.to/nightlyhermes/same-logs-no-secrets-penv-100-rc3-38ko</guid>
      <description>&lt;p&gt;This is a debug log most services have somewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO   shop-api 2.4.1 · production
DEBUG  db postgres://shop:vR2x9Lq7Tz4W@prod-db/shop
DEBUG  stripe sk_live_51Nq8xZ2eVbT4kP0aLm
INFO   listening on :3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It ends up in terminal scrollback, CI logs, a pasted bug report, and the context of any coding agent that ran the command. The clip below takes that app from &lt;code&gt;node --env-file=.env&lt;/code&gt; to &lt;code&gt;penv run&lt;/code&gt; in about 30 seconds. Everything in it runs locally, with no account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80w7u1u8nc33btd3dbmn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80w7u1u8nc33btd3dbmn.gif" alt="penv run masking secrets in server logs, then penv scan finding a leaked key" width="199" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Write the schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads &lt;code&gt;.env&lt;/code&gt;, writes &lt;code&gt;.env.schema&lt;/code&gt; (key, type, required, sensitive), adds &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.*&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;, and writes deny rules for any coding agent it detects. In the clip that is &lt;code&gt;.claude/settings.json&lt;/code&gt;. The schema is a plain &lt;a href="https://varlock.dev" rel="noopener noreferrer"&gt;@env-spec&lt;/a&gt; file, the same format &lt;a href="https://varlock.dev" rel="noopener noreferrer"&gt;varlock&lt;/a&gt; reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Run through penv
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv run &lt;span class="nt"&gt;--&lt;/span&gt; node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs are the same, but the secret values are masked. Masking is on by default in every &lt;code&gt;penv run&lt;/code&gt;. It also works inside Node, Bun, Deno and Python through a preload, so it covers &lt;code&gt;console&lt;/code&gt; and &lt;code&gt;Response&lt;/code&gt; bodies, not only the terminal.&lt;/p&gt;

&lt;p&gt;If git tracks the &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;penv run&lt;/code&gt; says so and prints the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-qm&lt;/span&gt; &lt;span class="s2"&gt;"stop tracking .env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Find the copies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;penv scan&lt;/code&gt; finds secret values that were committed to other files. In the clip it catches &lt;code&gt;config/staging.env.bak:3&lt;/code&gt; holding &lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;. Remove the value, rotate it if it was ever pushed, and read it from &lt;code&gt;penv run&lt;/code&gt; instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  rc.3: masking broke Next.js middleware
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;mask = true&lt;/code&gt;, the generated &lt;code&gt;env.ts&lt;/code&gt; and the &lt;code&gt;penv run&lt;/code&gt; preload mask &lt;code&gt;Response&lt;/code&gt; bodies through a subclass of &lt;code&gt;Response&lt;/code&gt;. A &lt;code&gt;NextResponse&lt;/code&gt; built on the original class then failed Next's &lt;code&gt;instanceof Response&lt;/code&gt; check, and every request answered 500. rc.3 keeps &lt;code&gt;instanceof Response&lt;/code&gt; working.&lt;/p&gt;

&lt;p&gt;Behaviour has been frozen since &lt;a href="https://github.com/penvhq/penvhq/releases/tag/v1.0.0-rc.1" rel="noopener noreferrer"&gt;1.0.0-rc.1&lt;/a&gt;. Only fixes land before 1.0.0. See the &lt;a href="https://penv.cloud/docs/changelog/cli?ref=devto" rel="noopener noreferrer"&gt;stability promise&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  With penv.cloud
&lt;/h3&gt;

&lt;p&gt;In cloud mode, &lt;a href="https://github.com/penvhq/penvhq/releases/tag/v1.0.0-rc.2" rel="noopener noreferrer"&gt;rc.2&lt;/a&gt; adds checks on what penv injects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A key that would change how the command runs, such as &lt;code&gt;NODE_OPTIONS&lt;/code&gt;, &lt;code&gt;LD_PRELOAD&lt;/code&gt;, &lt;code&gt;BASH_ENV&lt;/code&gt;, &lt;code&gt;PATH&lt;/code&gt; or &lt;code&gt;PIP_INDEX_URL&lt;/code&gt;, fails the run with &lt;code&gt;reserved_name&lt;/code&gt; (exit 3) instead of being injected.&lt;/li&gt;
&lt;li&gt;Your command never inherits a CI runner's OIDC request token, &lt;code&gt;GITHUB_ENV&lt;/code&gt;/&lt;code&gt;GITHUB_OUTPUT&lt;/code&gt;/&lt;code&gt;GITHUB_PATH&lt;/code&gt;/&lt;code&gt;GITHUB_STATE&lt;/code&gt;, &lt;code&gt;INPUT_*&lt;/code&gt; or &lt;code&gt;STATE_*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;penv login&lt;/code&gt; makes you type the code your terminal shows. A link alone can't approve someone else's terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://penv.cloud/?ref=devto" rel="noopener noreferrer"&gt;penv.cloud&lt;/a&gt; opens soon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&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
penv upgrade next     &lt;span class="c"&gt;# already installed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://penv.cloud/docs?ref=devto" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://penv.cloud/docs/changelog/cli?ref=devto" rel="noopener noreferrer"&gt;CLI changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/penvhq/penvhq/releases" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI from the penv &lt;a href="https://github.com/penvhq/penvhq/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; and checked against them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>rust</category>
      <category>ai</category>
    </item>
    <item>
      <title>penv 1.0.0-beta.1: project and env commands, a terminal UI, and what "beta" means here</title>
      <dc:creator>Oluwafemi</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:43:50 +0000</pubDate>
      <link>https://dev.to/nightlyhermes/penv-100-beta1-project-and-env-commands-a-terminal-ui-and-what-beta-means-here-5cld</link>
      <guid>https://dev.to/nightlyhermes/penv-100-beta1-project-and-env-commands-a-terminal-ui-and-what-beta-means-here-5cld</guid>
      <description>&lt;p&gt;Your coding agent can read &lt;code&gt;.env&lt;/code&gt;. It also reads what it prints.&lt;/p&gt;

&lt;p&gt;penv is a single Rust binary that validates a schema before your process starts, and keeps secret values out of files, the repo, shell history and agent output. 1.0.0-beta.1 shipped today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @penvhq/cli@next
&lt;span class="c"&gt;# or&lt;/span&gt;
curl penv.cloud/install | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/penvhq/penvhq" rel="noopener noreferrer"&gt;github.com/penvhq/penvhq&lt;/a&gt; · Release: &lt;a href="https://github.com/penvhq/penvhq/releases/tag/v1.0.0-beta.1" rel="noopener noreferrer"&gt;v1.0.0-beta.1&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start without an account
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv init
penv run &lt;span class="nt"&gt;--&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;penv init&lt;/code&gt; writes one committed file, &lt;code&gt;.env.schema&lt;/code&gt;. It is a &lt;a href="https://varlock.dev/env-spec/overview/" rel="noopener noreferrer"&gt;varlock &lt;code&gt;@env-spec&lt;/code&gt;&lt;/a&gt; file; the decorators (&lt;code&gt;@type&lt;/code&gt;, &lt;code&gt;@required&lt;/code&gt;, &lt;code&gt;@sensitive&lt;/code&gt;, …) are documented at &lt;a href="https://varlock.dev/env-spec/reference/" rel="noopener noreferrer"&gt;varlock.dev/env-spec/reference&lt;/a&gt;. Migrating an existing &lt;code&gt;.env&lt;/code&gt; into one: &lt;a href="https://dev.to/nightlyhermes/you-can-migrate-your-existing-env-into-varlocks-envschema-using-penv-guide-1b88"&gt;this guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A value that fails the schema exits 3 before &lt;code&gt;npm run dev&lt;/code&gt; starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under an agent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detects the running harness and writes its deny rules for &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.*&lt;/code&gt;. Claude Code, Codex, Cursor, Copilot CLI, Gemini, Amp, Cline, Windsurf. &lt;code&gt;--all&lt;/code&gt; writes every one. What each harness actually reads: &lt;a href="https://dev.to/nightlyhermes/eight-coding-agents-eight-deny-list-formats-what-each-one-actually-reads-3egj"&gt;Eight coding agents, eight deny-list formats&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Inside an agent session, &lt;code&gt;penv run&lt;/code&gt; masks secret values in the child's stdout and stderr: raw, hex, base64 and URL-encoded forms. &lt;code&gt;penv reveal&lt;/code&gt; is refused. Output is JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  New in beta.1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;penv project&lt;/code&gt; and &lt;code&gt;penv env&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penv project &lt;span class="nb"&gt;ls&lt;/span&gt; | new | rename | &lt;span class="nb"&gt;rm
&lt;/span&gt;penv &lt;span class="nb"&gt;env     ls&lt;/span&gt; | new | rename | copy | &lt;span class="nb"&gt;rm
&lt;/span&gt;penv &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-p &amp;lt;project&amp;gt;&lt;/code&gt; is optional inside a linked folder. &lt;code&gt;rm&lt;/code&gt; asks a person to type the name. From an agent or a pipe it exits 4 and prints the command to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminal UI
&lt;/h3&gt;

&lt;p&gt;Pickers, spinners on network calls, tables, and refusals written as sentences. Drawn on stderr, only when a person is at a TTY. Piped, JSON and agent sessions get the same plain lines as before.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;penv push&lt;/code&gt; refuses before the server is contacted
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;refusal&lt;/th&gt;
&lt;th&gt;when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nothing_to_push&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; is missing or has no values. An empty first push no longer creates a project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no_such_environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--env&lt;/code&gt; names an environment the project does not have. Lists the ones that exist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;org_mismatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--org&lt;/code&gt; contradicts the schema's &lt;code&gt;@penv=&lt;/code&gt; header. Previously ignored.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A new project is created with the environment being pushed, not only &lt;code&gt;development&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;penv pull&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Values containing quotes and line breaks round-trip, written in whichever literal quote the value lacks.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;\r&lt;/code&gt; in the file no longer blocks the pull.&lt;/li&gt;
&lt;li&gt;A partial write reports the keys it skipped instead of failing the whole pull.&lt;/li&gt;
&lt;li&gt;A folder with no &lt;code&gt;@penv=&lt;/code&gt; header can be linked to a project the account already has.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Also
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic value injection fixed for Next.js.&lt;/li&gt;
&lt;li&gt;Release notes are generated from merged PRs; npm provenance verifies against &lt;code&gt;penvhq/penvhq&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What beta means here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No more breaking changes planned before 1.0.&lt;/li&gt;
&lt;li&gt;Bugs expected. &lt;a href="https://github.com/penvhq/penvhq/issues" rel="noopener noreferrer"&gt;Issues&lt;/a&gt; are open.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@since&lt;/code&gt;, &lt;code&gt;@rotate&lt;/code&gt; and &lt;code&gt;@dynamicFrom&lt;/code&gt; semantics get documented at RC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT. One static binary. No runtime.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this post was generated by an AI from the &lt;a href="https://github.com/penvhq/penvhq/releases" rel="noopener noreferrer"&gt;release changelog on GitHub&lt;/a&gt; and reviewed before publishing. If anything here disagrees with the code, the changelog is right and I'd appreciate a comment pointing it out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>cli</category>
    </item>
    <item>
      <title>You can migrate your existing .env into varlock’s .env.schema using penv 

Guide: https://github.com/penvhq/penvhq</title>
      <dc:creator>Oluwafemi</dc:creator>
      <pubDate>Sat, 19 Sep 2026 07:48:22 +0000</pubDate>
      <link>https://dev.to/nightlyhermes/you-can-migrate-your-existing-env-into-varlocks-envschema-using-penv-guide-1b88</link>
      <guid>https://dev.to/nightlyhermes/you-can-migrate-your-existing-env-into-varlocks-envschema-using-penv-guide-1b88</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/penvhq/penvhq" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frepository-images.githubusercontent.com%2F1302485189%2F8e4e9756-b3ac-4fb9-8643-982129ad50e9" height="1280" class="m-0" width="1280"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/penvhq/penvhq" rel="noopener noreferrer" class="c-link"&gt;
            GitHub - penvhq/penvhq: The penv cli, a schema-first secrets tooling for humans and coding agents. One static binary: validates .env before your process starts, generates typed access, and guards agent harnesses. · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            The penv cli, a schema-first secrets tooling for humans and coding agents. One static binary: validates .env before your process starts, generates typed access, and guards agent harnesses. - penvhq...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Eight coding agents, eight deny-list formats. What each one actually reads.</title>
      <dc:creator>Oluwafemi</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:41:07 +0000</pubDate>
      <link>https://dev.to/nightlyhermes/eight-coding-agents-eight-deny-list-formats-what-each-one-actually-reads-3egj</link>
      <guid>https://dev.to/nightlyhermes/eight-coding-agents-eight-deny-list-formats-what-each-one-actually-reads-3egj</guid>
      <description>&lt;p&gt;Every coding-agent harness has a deny list. Almost nobody sets it, because each tool wants a different file, a different syntax and a different idea of what "deny" means.&lt;/p&gt;

&lt;p&gt;I went through all of them while building &lt;code&gt;penv guard&lt;/code&gt;. This is the audit, as of September 2026. Use it even if you never install penv.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threat is boring
&lt;/h2&gt;

&lt;p&gt;Your agent runs as you. If you can &lt;code&gt;cat .env&lt;/code&gt;, so can it. It doesn't need to be malicious: a grep for context, an &lt;code&gt;env&lt;/code&gt; while debugging, a stack trace with the environment attached. The value ends up in a transcript on someone else's server.&lt;/p&gt;

&lt;p&gt;A deny rule stops the &lt;em&gt;read&lt;/em&gt;. It does not stop the value leaking once it's in the process. Keep both problems in your head; this post is only about the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each harness wants
&lt;/h2&gt;

&lt;p&gt;Two patterns, everywhere: &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.*&lt;/code&gt;. Not &lt;code&gt;*.env&lt;/code&gt;, not &lt;code&gt;.env.local&lt;/code&gt; spelled out. The second one is the whole point: a rule on &lt;code&gt;.env&lt;/code&gt; alone does nothing for &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.production&lt;/code&gt;, &lt;code&gt;.env.bak&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.claude/settings.json&lt;/code&gt; (project). Two mechanisms, and you want both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"denyRead"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./.env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./.env.*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penv hook claude-code"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;permissions.deny&lt;/code&gt; is checked by the tool layer. &lt;code&gt;sandbox.filesystem.denyRead&lt;/code&gt; is enforced by the OS sandbox on macOS and Linux. Native Windows has no Claude Code sandbox, so only the deny rules and the hook apply there.&lt;/p&gt;

&lt;p&gt;There's also a user-scope file, &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, where you can list env vars to mask with &lt;code&gt;{"name": KEY, "mode": "mask"}&lt;/code&gt;. penv prints that block and tells you to paste it; it does not write outside your repo.&lt;/p&gt;

&lt;p&gt;The hook answers on &lt;strong&gt;stdout&lt;/strong&gt; with &lt;code&gt;permissionDecision: "deny"&lt;/code&gt; and &lt;strong&gt;exit 0&lt;/strong&gt;. Get that wrong and the hook is ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codex
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.codex/config.toml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sandbox_workspace_write]&lt;/span&gt;
&lt;span class="py"&gt;deny_read&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"**/.env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"**/.env.*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[shell_environment_policy]&lt;/span&gt;
&lt;span class="py"&gt;inherit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"core"&lt;/span&gt;
&lt;span class="py"&gt;ignore_default_excludes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Glob, not path. &lt;code&gt;**/&lt;/code&gt; because Codex resolves from the workspace root. The &lt;code&gt;shell_environment_policy&lt;/code&gt; block matters as much as the deny: &lt;code&gt;inherit = "core"&lt;/code&gt; keeps Codex from handing your whole environment to every subprocess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Two files. &lt;code&gt;.cursor/cli.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(.env.*)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;.cursor/hooks.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"beforeReadFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penv hook cursor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"failClosed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"beforeShellExecution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penv hook cursor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"failClosed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;failClosed: true&lt;/code&gt; is the line that matters. Without it, a hook that crashes is a hook that allows.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Copilot CLI
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.github/copilot/permissions-config.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(**/.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(**/.env.*)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest note, and it's in penv's own template comment: Copilot hasn't published a schema for this file. The shape above is best effort until they do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini CLI
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.gemini/settings.json&lt;/code&gt;. Hook on &lt;code&gt;run_shell_command&lt;/code&gt;, &lt;code&gt;PreToolUse&lt;/code&gt;. Same idea as Claude Code, different matcher name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amp
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.amp/settings.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amp.guardedFiles.allowlist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amp inverts the model: it guards files by default and you &lt;em&gt;allow&lt;/em&gt; exceptions. An empty allowlist means nothing is exempt. penv only writes this file if it's absent, so an allowlist you already curated is left alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cline
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.clinerules/hooks/PreToolUse&lt;/code&gt;. A two-line &lt;code&gt;/bin/sh&lt;/code&gt; script:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;penv hook cline &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Must be executable. Denies on stderr, exit 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windsurf
&lt;/h3&gt;

&lt;p&gt;File: &lt;code&gt;.windsurf/hooks.json&lt;/code&gt;. Two hooks, &lt;code&gt;pre_run_command&lt;/code&gt; and &lt;code&gt;pre_read_code&lt;/code&gt;, both pointing at &lt;code&gt;penv hook windsurf&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I got wrong the first time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The hook must be a binary, not a script.&lt;/strong&gt; A &lt;code&gt;node hook.js&lt;/code&gt; or &lt;code&gt;python hook.py&lt;/code&gt; fails open when the interpreter is missing or the wrong version. &lt;code&gt;penv hook &amp;lt;harness&amp;gt;&lt;/code&gt; is the penv binary itself; the only script in the whole set is Cline's two-line shim, because that's the only shape Cline accepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deny replies are not uniform.&lt;/strong&gt; Claude Code and Cursor want the decision on stdout with exit 0. Everyone else wants stderr and exit 2. Answer on both streams, or neither, and you're guessing what the harness does with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty stdin is the only allow.&lt;/strong&gt; If the hook can't parse the payload but there was something to match on, it refuses. Fail closed, always.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;penv guard&lt;/code&gt; does with all this
&lt;/h2&gt;

&lt;p&gt;It doesn't write eight files onto your machine. It probes for what's installed: a &lt;code&gt;.claude&lt;/code&gt; or &lt;code&gt;~/.claude&lt;/code&gt; folder, a &lt;code&gt;codex&lt;/code&gt; on PATH, and so on for each. Then it writes the guard for those and only those.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;penv guard
&lt;span class="go"&gt;HARNESS      INSTALLED  FILE                    STATUS
claude-code  yes        .claude/settings.json   written
codex        yes        .codex/config.toml      written
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;penv guard --check&lt;/code&gt; shows the same table without writing, and exits non-zero if anything is stale, so it works as a CI step. &lt;code&gt;penv guard cursor&lt;/code&gt; targets one. &lt;code&gt;penv guard --all&lt;/code&gt; writes every harness penv knows, for a template repo.&lt;/p&gt;

&lt;p&gt;Merges are per-file, not per-harness. JSON deny lists are unioned with what you already have. TOML and the Cline script are append-unique. Amp is replace-if-absent. Your existing config survives.&lt;/p&gt;

&lt;p&gt;Two invariants are tested on every render: the patterns are exactly &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.*&lt;/code&gt;, never a list of filenames; and no rendered guard may contain &lt;code&gt;.env.local&lt;/code&gt; (enumerating filenames is how you miss one) or &lt;code&gt;.env.schema&lt;/code&gt; (the schema is the one file the agent &lt;em&gt;should&lt;/em&gt; read).&lt;/p&gt;

&lt;h2&gt;
  
  
  The part the deny list can't do
&lt;/h2&gt;

&lt;p&gt;Once a value is in your process, no deny rule helps. That's the other half: &lt;code&gt;penv run -- your-app&lt;/code&gt; injects values at exec time and, when it detects an agent driving, masks every secret in the child's stdout and stderr in 14 encodings (raw, hex, base64 at three phases, URL-encoded, JSON-escaped). Different post.&lt;/p&gt;

&lt;p&gt;The security claim penv prints, verbatim, from &lt;code&gt;penv guard --check&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;penv validates your .env, keeps values out of your agent's output, and blocks it from reading the file where its harness allows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Where its harness allows" is doing real work in that sentence. That's this post.&lt;/p&gt;




&lt;p&gt;penv is MIT, one static Rust binary, no account needed for local mode.&lt;/p&gt;

&lt;p&gt;OSS: &lt;a href="https://github.com/penvhq/penvhq" rel="noopener noreferrer"&gt;https://github.com/penvhq/penvhq&lt;/a&gt;&lt;br&gt;
Install: &lt;code&gt;curl -fsSL penv.cloud/install | sh&lt;/code&gt; or &lt;code&gt;npm i -g @penvhq/cli&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If your harness isn't in the list, or one of these shapes is wrong, say so in the comments. The guard folder is data, so a fix is a PR to one &lt;code&gt;guard.toml&lt;/code&gt;.&lt;/p&gt;

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