How to protect secrets when coding agents deploy your app

By

Learn how coding agents can expose secrets, compare ways to manage access, and add safeguards to builds, deployment pipelines, and production code.

~~~

A coding agent can build your app correctly and still publish your database password.

It might copy a .env file into the upload directory. It might put a private API key in browser JavaScript to make a feature work. Or it might run a debugging command that sends credentials into the conversation history.

There is another risk that is easier to miss. You can hide a production secret from the agent and still let it deploy server code that reads that secret. The agent doesn’t need to know the value beforehand to expose it through a new endpoint.

Keeping secrets out of Git covers only one route. We need to control what the agent can read, which operations it can perform, and what happens between a code change and a production deployment.

I’ll use a small web app as the example: a reading club with a public website, a server endpoint, and a private database credential. The same questions apply to a mobile backend, a CLI release, or a larger application.

What counts as a secret?

A secret is a value that gives someone access or proves an identity, and therefore needs restricted handling.

API tokens, private keys, session cookies, database passwords, and webhook signing secrets all belong here. A database connection URL can contain a password even if its name doesn’t mention credentials.

Some configuration is intentionally public. A public API hostname, an analytics site identifier, or a provider’s publishable client key may belong in browser code. Check the provider’s documentation before deciding.

The distinction is what the value authorizes. A key designed for browser use still needs the provider’s intended restrictions. Calling a value “public” doesn’t make an administrative token safe to expose.

For our reading club:

ValueWho needs it?Where I would put it
Public website URLBrowser and serverPublic configuration
Database passwordServer applicationRuntime secret store
Deployment credentialApproved publisherIsolated deployment job
Test database credentialTest runnerSeparate development environment
Webhook signing secretServer handling that webhookRuntime secret store

Customer records and private documents also need protection. They are sensitive data even when they aren’t credentials. A read-only database role can still disclose every row it is allowed to read.

How an agent gets access

A coding agent usually combines a language model with tools. The tools can read files, run shell commands, open a browser, and call APIs.

There are several levels of exposure:

ExposureExampleWhat it allows
The model sees the valueA command prints an API tokenThe value can enter the conversation and subsequent outputs
An agent-controlled process sees itA shell inherits a database passwordCommands can use or copy it without printing it first
The agent can use an authenticated toolA deploy tool holds a token internallyThe agent can perform whatever operations that tool permits
The agent can replace privileged application codeA deployment updates a server with secret bindingsNew code can use those runtime credentials

Hiding a value from the model is useful. It reduces accidental inclusion in answers and logs. We also need to limit the authority available through the other routes.

For example, a deploy tool can keep its token outside the model’s context. If that tool accepts an arbitrary account, destination, and shell command, its interface still gives the agent too much control.

Why coding agents make this problem harder

Developers have always leaked secrets. Agents add a workflow where one program can discover files, change code, run it, and publish the result in the same session.

An ordinary debugging sequence can cross several boundaries. The agent finds an authentication error, reads local configuration, prints the environment to investigate, and copies an example into the frontend. Each step can seem related to the task.

Build tools and dependencies also execute code. Even if the model follows your instructions, an install script or changed build script can read credentials made available to that process.

Then there is prompt injection. The agent may read instructions embedded in an issue, a repository file, a web page, or tool output. Those instructions can try to redirect its tools toward reading or sending secrets. OWASP’s prompt injection guidance recommends enforcing permissions and validating tool parameters outside the model.

An AGENTS.md rule saying “never read .env” helps communicate your intent. A filesystem restriction that prevents the read gives you a boundary you can test. Use both, and check whether the shell tool has the same restrictions as the file-reading tool.

Map where a secret could escape

For the reading club, I would trace a credential through the entire development and deployment flow.

PlaceCommon exposureGuard to consider
Working directory.env, database dumps, copied credentialsSeparate workspace and restricted file access
Process environmentInherited tokens, env output, debug tracesStart with a minimal environment and scoped identity
Agent conversationPasted secrets, command output, screenshotsKeep values out of prompts and tool responses
GitSource files, old commits, configurationSecret scanning and push protection
Build outputCopied files, HTML, JavaScript, source mapsInspect and scan the final public artifact
CIUntrusted scripts run beside deployment tokensSeparate build and publish jobs
Production runtimeNew code reads an existing secretReview privileged code changes and restrict runtime authority
Logs and storageError reports, caches, uploaded artifactsRedaction, restricted access, retention controls

A secret scanner at the Git boundary covers only one row. A token can stay out of Git and still end up in a deployed file or a CI log.

Start by giving the agent less to read

For a normal coding task, the agent needs the repository and a development environment. It rarely needs your whole home directory or every credential in your shell.

Use an isolated workspace, container, VM, or dedicated operating-system account. Mount the project and required tools. Leave unrelated credentials outside it.

Check the less obvious connections too: SSH agents, cloud CLI profiles, credential helpers, browser profiles, Docker sockets, and access to a host filesystem. A container with powerful host mounts can inherit more authority than you intended.

Environment variables are another input. Launching an agent from your everyday shell may pass along tokens exported earlier. Give it only the variables its development task needs. Cleaning the environment reduces exposure, but it doesn’t stop a process from reading credential files it can still access.

Don’t assume .gitignore restricts reading. Git uses it to decide which untracked files to ignore. A shell command or build tool can still open those files.

The same applies to product-specific ignore files. Verify what they affect. Excluding a file from indexing doesn’t necessarily prevent a command from reading it.

A useful first test is to place a harmless marker outside the permitted project directory. Ask the agent’s file tool and shell tool to read it. Both should be denied if that is the boundary you intend to enforce.

Use development credentials and synthetic data

The reading club can be built against a test database containing invented books and members.

Give the agent a separate development identity. Scope it to that database, with only the permissions required for the task. Use a separate provider project or account where that creates a clearer boundary.

A development token should not be able to list production secrets, change production permissions, or create a more powerful identity. Restrict administrative APIs as well as ordinary data access.

For an investigation that needs production information, first ask whether sanitized logs or a narrow query result will answer the question. If direct access is necessary, make it temporary, restrict the dataset, and audit the operation.

“Read-only” describes allowed changes. It doesn’t make the data safe to hand to an agent. OWASP’s secrets management guidance covers access scope, rotation, and the lifecycle of these credentials.

Choose how secrets reach trusted code

A secret manager helps avoid scattered plaintext files and gives you a place to control access and rotation.

Different tools solve different parts of the problem:

ApproachExamplesMain boundary to protect
Local secret delivery1Password CLIThe process receiving plaintext
Cloud secret storeAWS, Google, AzureIdentity policies and runtime code
Dynamic credentialsVaultRole scope and credential lifetime
Runtime bindingsCloudflare WorkersCode allowed to use the bindings
Restricted servicePublisher or API proxyAllowed operations and data flows

AWS Secrets Manager, Google Secret Manager, and Azure Key Vault document their access and operational controls. Choose the one that fits your infrastructure before adding another system to maintain.

Vault’s database secrets engine can generate database credentials dynamically. A short lease limits how long a copied credential remains useful, provided the database integration and revocation behavior match your needs.

For local development, see my 1Password environment variables guide. For browser logins, password managers and agent access cover a different boundary.

Injection into a process still exposes the value to that process

1Password’s op run resolves references and makes secrets available as environment variables in a subprocess.

That keeps the value out of a checked-in environment file. The subprocess can still read it.

If you run an unrestricted agent inside that subprocess, its commands may be able to read or transmit the same value. Output masking helps with accidental printing; it doesn’t restrict what the process can do with the credential.

Treat injection as a delivery method. Decide separately whether you trust the recipient with the value.

A restricted service can keep the credential elsewhere

Suppose the agent only needs to publish the reading club’s static site. A small trusted publisher can hold the deployment token and accept an operation such as “publish this approved artifact to the reading-club site.”

The publisher should choose the account and destination from protected configuration. It should validate the artifact identity and reject arbitrary command arguments, shell fragments, and alternate upload locations.

For other operations, an API proxy can attach a credential to requests for specific hosts and paths. It must restrict the request body and response too. An allowed API call can still reveal customer data or perform an unwanted action.

This design takes more work than handing over a token. It becomes useful when you need unattended execution with a narrow, repeatable set of operations.

Replace long-lived deployment tokens where possible

With workload identity, a CI job can prove where it is running and obtain temporary access.

GitHub Actions supports OpenID Connect for exchanging a job’s identity token for credentials from a compatible provider. This avoids storing a long-lived cloud credential in the repository’s CI secrets.

The trust policy is essential. Restrict the accepted issuer and audience, and the subject or other supported claims that identify the intended repository and deployment context. A policy that trusts every branch or every repository defeats much of the benefit.

Grant the GitHub Actions id-token: write permission only to the job that needs federation. That job still requires narrowly scoped provider permissions.

Distinguish temporary access to a secret manager from the lifetime of the secret it returns. A permanent API key copied during a five-minute job can remain valid after the job ends.

A short-lived credential can do damage during its lifetime. If untrusted code runs in a job that can request production access, OIDC has not made that job safe.

If an OIDC subject identifies an environment rather than a branch, enforce the allowed branches through the environment’s protection rules too.

Some deployment targets don’t support the federation path you want. Use a separate, scoped deploy token there, keep it in the protected publisher, and document how to revoke it. Don’t assume a platform supports OIDC because your CI system does.

Make the public artifact explicit

The project folder contains source files, local configuration, dependencies, and build output. Only a defined part of it belongs on the public website.

For a static Astro site, the usual output is dist/. Configure the publisher to upload that directory. Avoid upload commands that derive their destination directory from an agent-generated argument or default to the project root.

Astro copies files in public/ into the build untouched. A secret placed there can become a downloadable file even if Git ignores it.

Fig 1 · 1/4What crosses into public files
Build the public artifact first

The exact artifact boundary depends on the deployment target. An SSR deployment can have public assets and a server bundle. A container image has its own filesystem and layers. Identify what becomes downloadable, what executes privately, and what each part can read.

Inspect the final package after all build and packaging steps. A clean dist/ doesn’t help if a later ZIP command adds the rest of the workspace.

Source maps deserve a deliberate decision. They can reveal source code and embedded values. They aren’t automatically secrets, and some applications publish them intentionally. Check their content and audience rather than assuming the .map extension is harmless or always forbidden.

Keep private values out of browser code

Astro’s environment variable documentation explains that PUBLIC_ variables can be exposed to client-side code.

For the reading club, PUBLIC_API_URL can point at the server. The database password belongs on the server.

If the agent proposes renaming a private token to PUBLIC_DATABASE_TOKEN to fix an undefined variable, stop at that change. Move the privileged operation behind an authenticated server endpoint instead.

Then validate what that endpoint allows. A server proxy that accepts arbitrary database queries or arbitrary upstream URLs gives callers too much authority even though the password remains hidden.

Server-only names don’t protect a value after code copies it into HTML, a JSON response, a log, or a browser bundle. Review where values flow, as well as how they are declared.

Scan source and deployment output separately

I would use a maintained scanner for recognizable credentials, plus project-specific artifact rules.

Gitleaks can scan Git history and files on disk. With Gitleaks installed, run these from the project root:

gitleaks git --redact=100 --no-banner .
gitleaks dir --redact=100 --no-banner dist

The first checks Git history. The second checks the output directory, including generated files that might never have been committed.

Gitleaks returns a nonzero status when it finds leaks by default. Configure CI to fail on a finding or scanner error. Don’t turn the command into a warning with || true or continue-on-error.

Full redaction reduces exposure through reports. Restrict access to scanner logs anyway, and verify how the scanner handles your file types, archives, and size limits. Archive traversal requires its own configuration in Gitleaks.

The policy should come from a trusted location. An agent that can change scanner rules, add ignore entries, and approve its own build can weaken the check it is supposed to pass.

GitHub’s push protection adds a guard before supported secrets enter a repository. Review bypass permissions and exceptions. It complements artifact scanning because it cannot inspect bytes generated only during a later build.

A scanner cannot recognize every credential. A custom token format, transformed value, or split string can escape detection. Reduce secret availability before depending on detection.

Test the artifact boundary with fake secrets

You can test rejection without using a credential that grants access anywhere.

Save the following as artifact-demo.mjs. It creates a temporary build directory and checks an intentionally tiny file policy. The marker is invented.

import assert from 'node:assert/strict'
import { mkdtemp, mkdir, writeFile, readFile, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

const root = await mkdtemp(join(tmpdir(), 'artifact-demo-'))
const output = join(root, 'dist')
const marker = 'DEMO_ONLY_DO_NOT_USE'
const allowed = new Set(['index.html', 'app.js'])

async function check() {
  const entries = await readdir(output, { withFileTypes: true })
  if (entries.length !== allowed.size) throw new Error('Unexpected artifact entries')
  for (const entry of entries) {
    if (!entry.isFile() || !allowed.has(entry.name)) {
      throw new Error('Unexpected artifact entry')
    }
    const content = await readFile(join(output, entry.name), 'utf8')
    if (content.includes(marker)) throw new Error('Fixture marker exposed')
  }
}

try {
  await mkdir(output)
  await writeFile(join(output, 'index.html'), '<h1>Reading club</h1>')
  await writeFile(join(output, 'app.js'), `console.log('${marker}')`)
  await assert.rejects(check(), /Fixture marker/)

  await writeFile(join(output, 'app.js'), "console.log('Ready')")
  await writeFile(join(output, '.env.fixture'), `DEMO_TOKEN=${marker}`)
  await assert.rejects(check(), /Unexpected artifact/)

  await rm(join(output, '.env.fixture'))
  await check()
  console.log('Both unsafe fixtures rejected; clean artifact accepted')
} finally {
  await rm(root, { recursive: true, force: true })
}

Run it:

node artifact-demo.mjs

The example rejects the marker inside app.js, rejects an unexpected environment file, and accepts a clean fixture. It reads only files it created and doesn’t print their contents.

This is a demonstration of two checks: which files are present and what those files contain. The exact two-file allowlist is for this fixture only. It is not a production scanner for a real Astro build.

For a real project, define rules around your actual output. Reject credential filenames, private key files, unexpected archives, and symlinks unless your packaging policy explicitly supports them. Inspect nested directories. Decide how hidden files and compressed assets are handled.

Add a synthetic marker test to the real pipeline too. Put it in a harmless test input that follows the same route as configuration, build the app, and assert that the public artifact doesn’t contain it. Then deliberately place it in a browser asset and confirm that the check fails.

A generic secret scanner may not recognize your marker. Give the test an explicit marker check or a dedicated scanner rule, kept with the protected policy.

Separate building from publishing

The build job executes repository code. It installs dependencies, runs scripts, and processes files that an agent may have changed.

The publisher should have a narrower job: receive a specific artifact, verify the required checks and approval, then publish it to a fixed destination.

Separate an agent's changes from production publishingThe agent proposes source changes. A build without production credentials creates an artifact. Checks and review bind approval to that artifact before an isolated publisher deploys it.Agent proposessource changesBuild withdevelopment accessInspect and scanfinal artifactReview source andartifact identityApprove this releaseIsolated publisherdeploys artifactProtected deploymentcredential

For the reading club’s static site, I would give the build no production deployment credential and no production database password. The publisher would receive only the approved output and the credential required to update that site.

Use a fresh runner or another real isolation boundary between those jobs. Separate steps on one compromised machine can still share files or leave processes behind. A long-lived self-hosted runner needs particular care.

The publisher must not run the repository’s install or build scripts after receiving the credential. It should use trusted publishing code and configuration. Some deployment commands perform a build, load plugins, or execute hooks automatically; check yours before treating it as an upload-only operation.

For private package downloads, a build may need a credential. Give it a separate read-only credential limited to the package registry it needs. That is a reason to scope build access carefully, rather than hand it the deployment identity too.

GitHub’s Actions security guidance covers untrusted code, token permissions, third-party actions, and runner security. Pin third-party actions to reviewed full commit SHAs and control updates to those references.

Bind approval to the bytes being released

An approval should identify the source commit, artifact, destination, and relevant check results.

If the agent changes code or configuration after review, require a new review. If the pipeline rebuilds after approval, the resulting artifact may differ from the one you inspected.

Record the intended repository and build run alongside an immutable artifact identity and a cryptographic digest. Verify those against a protected release record, rather than accepting whichever artifact ID and checksum a caller supplies. The publisher should verify it and reject a mismatch. A digest proves which bytes you have; it doesn’t prove those bytes are safe.

GitHub’s artifact documentation describes digest validation during downloads. It also states that a mismatch produces a warning. If your policy requires blocking publication, add an explicit failure on mismatch rather than relying on that warning.

A signed artifact or provenance record can strengthen the link to its build identity. Keep the signing and verification policy outside the agent’s control. Signing a malicious build preserves evidence of its origin without making it trustworthy.

Protect the gate from the agent

The agent should be able to propose changes to deployment configuration. It should not be able to remove the controls that authorize those changes without independent review.

Protect workflow files, publishing scripts, scanner policies, and infrastructure permissions. Require the appropriate code owners or reviewers for those paths. Ensure the agent’s repository token cannot bypass the branch rules or impersonate the reviewer.

On GitHub, protected branch rules have bypass settings worth checking. An account with enough administration access can undermine a carefully written review policy.

A protected deployment environment can delay a job until required review completes. GitHub documents when environment secrets become available after approval. Availability of protection rules depends on the plan and repository visibility, so verify it for your repository.

For larger setups, put publishing policy in a separately controlled service or reusable workflow. The important question is who can change it and what code gets to execute with the production credential.

Don’t run untrusted pull-request code in a privileged context just because you need a convenient comment or status check. In particular, review uses of pull_request_target and privileged follow-up workflows before checking out or executing a contributor’s code.

Deployment access can become runtime secret access

This is the boundary I would pay the most attention to for a server application.

Cloudflare Worker secrets hide their values in the dashboard and Wrangler after they are set. Worker code receives the values at runtime.

Therefore, someone who can deploy replacement Worker code may be able to make it read and disclose an existing binding. That is a consequence of the runtime access model, even if the deployment API doesn’t offer a “read secret” operation.

The same question applies anywhere your application runs with an identity that can fetch secrets or access private data.

Deployment authority can reach runtime secretsAn agent allowed to change production server code can deploy code that reads a runtime secret and sends it in a response. Protecting access to secret values alone does not block this route.Agent can replaceserver codeNew code runs inproductionRuntime identity canread secretCode can return ortransmit value

For the reading club, keep the server’s database role limited to the operations it needs. Review changes to authentication, response bodies, outbound requests, and logging before those changes reach production.

If the agent only needs to update static pages, give it a publishing path that cannot replace the privileged backend. Separate applications or deployment identities can make that restriction concrete.

For a backend release, independent review of executable code is part of protecting its secrets. Scanning static assets cannot catch every runtime disclosure route.

My Cloudflare secrets and environments guide covers configuring those bindings. Their placement solves storage and delivery; deployment policy controls which code gets to use them.

Restrict outbound access where it helps

A process that can read a credential and make arbitrary network requests has an easy disclosure route.

For controlled agent environments, restrict outbound destinations to the services needed for the task. Apply this at the network, proxy, or tool boundary. Check DNS access and alternate network paths as well as HTTP requests.

This is useful defense in depth, but allowed destinations also need scrutiny. A Git hosting service can receive arbitrary repository content. An allowed logging endpoint can receive a credential in a log. A malicious response from your own app may be enough to leak data without a new external domain.

A credential-injecting proxy should attach secrets only for approved hosts and operations. It should also handle redirects safely so credentials don’t follow a redirect to another destination.

If you allow unrestricted shell access, check whether the agent can change proxy settings or bypass the network policy. The restriction needs to be enforced outside the process being restricted.

Don’t hide leaks with masking

Log masking is useful for known values. It cannot guarantee that every encoded, shortened, or transformed version will be removed. GitHub’s security guidance explicitly warns that automatic redaction is not guaranteed.

Avoid dumping the whole environment, request headers, or full configuration while debugging. Prefer a check such as “database credential is configured” that returns a status without its value.

Look beyond CI stdout. Secrets can reach test reports, crash dumps, screenshots, tracing systems, agent transcripts, preview deployments, caches, and retained build artifacts.

Restrict who can access those systems and how long they retain data. When a scanner finds a secret, report a rule identifier and location with the value redacted. Don’t paste the value into an issue to prove the finding.

Watch container builds too

If you deploy containers, deleting a secret in a later Dockerfile instruction may leave it in an earlier image layer.

Docker’s build secret documentation recommends secret mounts rather than build arguments or environment variables for build-time credentials.

A secret mount limits persistence through the build mechanism. The command using the mount can still copy the value into a file, print it, or send it over the network. Review the Dockerfile and the scripts that run while that mount is available.

Keep local secret files out of the build context using .dockerignore, and inspect the final image as well as repository files. Multi-stage builds help control what reaches the runtime image, but only if the files copied between stages are the ones you intended.

A practical set of guards

For a small application, I would start here:

GuardA useful test
Dedicated agent workspace with development credentialsFile and shell tools cannot read a marker outside the allowed workspace
Separate production identityDevelopment credentials cannot access production resources
Explicit public output directoryA fake .env file in the wrong location blocks publication
Source and artifact scanningA known synthetic detection fixture causes a failed check
Isolated publisherRepository scripts cannot run with the deployment credential
Independent production approvalThe agent cannot approve or bypass its own release
Artifact identity verificationA modified artifact is rejected after approval
Restricted runtime authorityThe application cannot access unrelated databases or secrets

Then add controls that match what you operate. A team handling sensitive data may need network restrictions, dynamic credentials, stronger audit retention, and a separately administered release service.

Test the negative cases. Try an incorrect destination, an expired credential, a changed artifact, and an unauthorized production request. Verify that the system refuses them without revealing values in the error output.

Use fake values for disclosure tests. Test authorization with a real restricted identity against a harmless protected resource, or use the provider’s policy simulator. An invalid dummy token cannot prove that the actual development identity lacks production access.

If a secret has already leaked

Revoke or rotate the credential at its provider. Treat deleting the file as cleanup, not proof that nobody copied it.

Remove the exposed deployment and check other versions, previews, downloaded artifacts, and caches. Follow the hosting provider’s cache-removal process where needed.

Review access logs for the credential and the resources it could reach. Look for use outside expected workloads, unexpected data access, and changes to permissions or identities.

Update legitimate consumers to the replacement credential. Where the leaked value can create other credentials or sessions, investigate and invalidate those too. The response depends on what that particular secret authorized.

Clean repository history, CI logs, and retained outputs where appropriate, while preserving incident evidence in a restricted location. My guide to exposed GitHub credentials covers the Git side of that recovery.

Finally, add a test at the boundary that failed. If public/.env shipped, make the artifact policy reject it. If a privileged job executed untrusted code, fix that execution path before restoring its access.

How I would set this up for the reading club

I would let the agent work freely in a development environment with synthetic member data. It could edit the app, run tests, and prepare a preview using development-only resources.

Production database access would belong to the server identity. Deployment access would belong to an isolated publisher with a fixed destination. The agent could prepare the release, but an independent review would authorize the server code and the exact artifact being published.

I would first test an accidental secret copy, an artifact changed after approval, and an attempt to deploy an unapproved backend change. Those failures tell me more about the setup than a successful deployment does.

If you want to work through the underlying concepts, my free Security Fundamentals course covers trust boundaries and incident response. The Software Supply Chain Security course follows the path through dependencies, CI, artifacts, and publishing.

Tagged: AI · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about ai: