<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Chandler Santos — writing by Chandler Gavin Santos</title>
    <link>https://www.chandlersantos.com/blog</link>
    <description>Personal site of Chandler Gavin Santos. Cybersecurity professional, builder of things for the Kingdom, and one super cool dude.</description>
    <language>en-us</language>
    <lastBuildDate>Tue, 05 May 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.chandlersantos.com/rss.xml" rel="self" type="application/rss+xml" />
    <image>
      <url>https://www.chandlersantos.com/opengraph-image</url>
      <title>Chandler Santos</title>
      <link>https://www.chandlersantos.com</link>
    </image>

    <item>
      <title>AI Agents and the Credentials They Shouldn&apos;t Have</title>
      <link>https://www.chandlersantos.com/blog/ai-agents-and-the-credentials-they-shouldnt-have</link>
      <guid isPermaLink="true">https://www.chandlersantos.com/blog/ai-agents-and-the-credentials-they-shouldnt-have</guid>
      <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
      <description>The three ways secrets leave your machine during an agent session, and the configs that actually stop it.</description>
      <dc:creator>Chandler Gavin Santos</dc:creator>
      <category>AI Development</category>
      <category>Environment</category>
      <media:content url="https://www.chandlersantos.com/blog/ai-agents-and-the-credentials-they-shouldnt-have/opengraph-image" type="image/jpeg" medium="image">
        <media:title>AI Agents and the Credentials They Shouldn&apos;t Have</media:title>
        <media:description>The three ways secrets leave your machine during an agent session, and the configs that actually stop it.</media:description>
      </media:content>
      <content:encoded><![CDATA[<p>Your `.env` file is not private from your AI agent. It&apos;s private from git. Those are different boundaries, and most developers are only protecting one of them.</p>
<p>When a coding agent opens your project, it reads the working directory to build context. That context goes into the inference call, which leaves your machine. `.gitignore` doesn&apos;t touch that path. Neither does your `CLAUDE.md`.</p>
<p>## CLAUDE.md won&apos;t protect you</p>
<p>Adding &quot;never read `.env` files&quot; to `CLAUDE.md` is the most common approach and the least reliable one. `CLAUDE.md` is advisory. Claude follows it most of the time, but under complex tasks, long context windows, or ambiguous instructions, it can and does deviate. A GitHub issue confirmed in April 2026: Claude reads and echoes `.env` contents into the conversation even when `CLAUDE.md` explicitly prohibits it.</p>
<p>The reliable protection is a deny rule in `settings.json`. Deny rules are enforced at the system level before Claude processes any file. The difference between &quot;please don&apos;t read this&quot; and &quot;you physically cannot read this.&quot;</p>
<p>## The three paths secrets leave</p>
<p>Most developers protect against the obvious one. The other two are where it actually happens.</p>
<p>### Direct file read</p>
<p>The agent scans your project, opens `.env`, and the contents become part of the conversation context. Blockable with deny rules in `settings.json`.</p>
<p>### Runtime output capture</p>
<p>The agent runs your tests or starts your server. A failed HTTP request logs the full `Authorization: Bearer sk-live-abc123...` header. A database timeout dumps the connection string with the password in it. Claude captures all command output. Your secrets are now in the conversation, even though the agent never opened `.env` directly.</p>
<p>### Search and grep</p>
<p>The agent uses grep to search the codebase for a function name. The search hits a config file containing credentials. The grep output includes the matched lines with secrets visible. No file read required.</p>
<p>&lt;ContextPathsFigure /&gt;</p>
<p>## The deny rules that actually work</p>
<p>Add these to `~/.claude/settings.json` for global protection across every project. This applies before Claude sees any file.</p>
<p>&lt;CodeLabel&gt;~/.claude/settings.json&lt;/CodeLabel&gt;</p>
<p>```json<br />{<br />  &quot;permissions&quot;: {<br />    &quot;deny&quot;: [<br />      // Environment and secrets files<br />      &quot;Read(**/.env*)&quot;,<br />      &quot;Read(**/.dev.vars*)&quot;,<br />      &quot;Write(**/.env*)&quot;,</p>
<p>// Key material<br />      &quot;Read(**/*.pem)&quot;,<br />      &quot;Read(**/*.key)&quot;,<br />      &quot;Read(**/secrets/**)&quot;,<br />      &quot;Read(**/credentials/**)&quot;,</p>
<p>// Cloud and system credential files<br />      &quot;Read(**/.aws/**)&quot;,<br />      &quot;Read(**/.ssh/**)&quot;,<br />      &quot;Read(**/.npmrc)&quot;,<br />      &quot;Read(**/.pypirc)&quot;,</p>
<p>// Database and app credentials<br />      &quot;Read(**/config/database.yml)&quot;,<br />      &quot;Read(**/config/credentials.json)&quot;,</p>
<p>// Dangerous write and exec operations<br />      &quot;Write(**/.ssh/**)&quot;,<br />      &quot;Write(.github/workflows/*)&quot;,<br />      &quot;Bash(rm -rf *)&quot;,<br />      &quot;Bash(sudo *)&quot;,<br />      &quot;Bash(curl * | sh)&quot;,<br />      &quot;Bash(wget *)&quot;,<br />      &quot;Bash(npm publish *)&quot;<br />    ]<br />  }<br />}<br />```</p>
<p>The `**` wildcard applies to every subdirectory. Claude physically cannot read any of these files regardless of what instructions it&apos;s given or what&apos;s in `CLAUDE.md`.</p>
<p>## MCP configs and CLI auth files</p>
<p>MCP server configurations hold credentials too: search API keys, database connection strings, OAuth tokens for whatever services you&apos;ve wired in. Those configs live in `mcp.json` or in agent settings directories the agent can read. They don&apos;t fall under the `.env` deny pattern above, which means they need their own entries.</p>
<p>Same applies to CLI tools that store auth in project-level config files. In my case I have Fizzy, my project board CLI, which writes a YAML config that holds auth tokens. Any CLI tool that persists config to the project directory is the same category of problem: it&apos;s a credential-bearing file that the agent can reach.</p>
<p>Add these to the deny block and to `.gitignore` by name, not by assumed pattern coverage:</p>
<p>&lt;CodeLabel&gt;.gitignore additions&lt;/CodeLabel&gt;</p>
<p>```gitignore<br /># MCP configs with credentials<br />mcp.json<br />.mcp.json<br />.claude/settings.local.json</p>
<p># CLI tool auth (add yours by name)<br />.fizzy.yaml<br />.fizzy/<br />*.local.yaml<br />*.local.json</p>
<p># Keep templates, not values<br />.env.example<br />.env<br />```</p>
<p>&lt;CodeLabel&gt;Additional deny rules for mcp.json in settings.json&lt;/CodeLabel&gt;</p>
<p>```json<br />&quot;Read(**/mcp.json)&quot;,<br />&quot;Read(**/.mcp.json)&quot;,<br />&quot;Read(**/.claude/settings.local.json)&quot;,<br />&quot;Read(**/*.local.yaml)&quot;<br />```</p>
<p>One thing worth watching: agents modify `.gitignore` during sessions without flagging it. When the agent scaffolds a new feature or fixes a config, check the diff on `.gitignore` before committing. Entries you had in there may have quietly dropped.</p>
<p>&lt;AgentReachFigure /&gt;</p>
<p>## Blocking runtime leaks</p>
<p>Deny rules stop direct file reads. They don&apos;t stop your service key appearing in a `curl` error log the agent captures when a test fails. For that, use test-specific environment files with dummy values and point your test runner at those instead of `.env`:</p>
<p>&lt;CodeLabel&gt;.env.test - commit this, safe to expose&lt;/CodeLabel&gt;</p>
<p>```dotenv<br /># Dummy values for test environments. No real credentials.<br />SUPABASE_URL=http://localhost:54321<br />SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test<br />SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test<br />STRIPE_SECRET_KEY=sk_test_not_a_real_key<br />OPENAI_API_KEY=sk-test-dummy-key-for-mocking<br />AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE<br />```</p>
<p>When the agent runs tests and something fails, the only credentials visible in the output are dummies. The actual key patterns never appear.</p>
<p>A brief note on key scope for Supabase specifically: the anon key respects RLS and is safe for agent-assisted development work. The service role key bypasses RLS entirely. Keep it out of any file in directories where you&apos;re running agents. If it&apos;s needed for a privileged operation, inject it at runtime through the Supabase CLI or a secrets tool, not from a `.env` file the agent can reach.</p>
<p>## Pre-commit hook</p>
<p>A last-line catch for when something slips through. This scans staged content for credential patterns before any commit reaches the repository:</p>
<p>&lt;CodeLabel&gt;.git/hooks/pre-commit&lt;/CodeLabel&gt;</p>
<p>```bash<br />#!/bin/bash</p>
<p>PATTERNS=(<br />  &apos;sk-ant-&apos;                # Anthropic API keys<br />  &apos;sk-live-&apos;               # Stripe live keys<br />  &apos;sk_live_&apos;               # Stripe live keys, alternate format<br />  &apos;ghp_&apos;                   # GitHub personal tokens<br />  &apos;gho_&apos;                   # GitHub OAuth tokens<br />  &apos;AKIA&apos;                   # AWS access keys<br />  &apos;xox[bpors]-&apos;            # Slack tokens<br />  &apos;SG\.&apos;                   # SendGrid keys<br />  &apos;eyJ&apos;                    # JWTs, base64 header<br />  &apos;BEGIN.*PRIVATE KEY&apos;     # Private key material<br />  &apos;supabase_service_role&apos;  # Supabase service keys<br />)</p>
<p>for pattern in &quot;${PATTERNS[@]}&quot;; do<br />  if git diff --cached --diff-filter=ACM | grep -qE &quot;$pattern&quot;; then<br />    echo &quot;BLOCKED: Found pattern matching &apos;$pattern&apos;&quot;<br />    echo &quot;Remove the credential before committing.&quot;<br />    exit 1<br />  fi<br />done</p>
<p>echo &quot;Pre-commit check passed.&quot;<br />exit 0<br />```</p>
<p>&lt;CodeLabel&gt;Make it executable&lt;/CodeLabel&gt;</p>
<p>```bash<br />chmod +x .git/hooks/pre-commit<br />```</p>
<p>Or use gitleaks as a pre-commit hook if you want maintained pattern coverage without managing the script yourself:</p>
<p>&lt;CodeLabel&gt;.pre-commit-config.yaml&lt;/CodeLabel&gt;</p>
<p>```yaml<br />repos:<br />  - repo: https://github.com/gitleaks/gitleaks<br />    rev: v8.22.1<br />    hooks:<br />      - id: gitleaks<br />```</p>
<p>## Runtime injection for production credentials</p>
<p>The pre-commit hook and deny rules handle what the agent can see. For credentials that shouldn&apos;t exist as files at all, runtime injection is the right model. The agent works with references, and the actual value gets resolved at process start from a store the agent can&apos;t reach.</p>
<p>&lt;CodeLabel&gt;Using Infisical CLI&lt;/CodeLabel&gt;</p>
<p>```bash<br /># Secrets injected at process start, never in a file.<br />infisical run --env=production -- node server.js<br />```</p>
<p>&lt;CodeLabel&gt;Your code references names, not values&lt;/CodeLabel&gt;</p>
<p>```ts<br />const supabase = createClient(<br />  process.env.SUPABASE_URL,<br />  process.env.SUPABASE_ANON_KEY<br />);<br />```</p>
<p>The agent generates code that references `process.env.SUPABASE_URL`. Even if that code ends up somewhere public, the reference without the store access is useless. Infisical, which is MIT licensed, and Doppler both do this. For Supabase-specific secret management, `supabase secrets set` pushes values directly to the project without ever touching a local file.</p>
<p>&lt;RuntimeInjectionFigure /&gt;</p>
<p>## Before your next session</p>
<p>- Deny rules for `.env*`, `*.pem`, `*.key`, `.aws/**`, and `.ssh/**` in `~/.claude/settings.json`.<br />- MCP config files, including `mcp.json` and `.mcp.json`, in both `.gitignore` and the deny list.<br />- CLI auth files for any tool that stores tokens in the project directory, added by name to `.gitignore`.<br />- `.env.test` with dummy values for test runs, so agent-triggered test output exposes those instead of real keys.<br />- A pre-commit hook, either gitleaks or the script above, scanning staged content for credential patterns.<br />- Production credentials kept out of files and injected at runtime through Infisical, Doppler, or `supabase secrets set`.<br />- `.gitignore` audited after any session where the agent touched project config files.</p>
<p>The mental model worth internalizing: your agent&apos;s context window is a pipe to a third-party server. Anything it can reach from disk may go into that pipe. `.gitignore` handles the repository. The deny rules handle what the agent can read. You have to handle what it runs.</p>]]></content:encoded>
    </item>
  </channel>
</rss>