7 min read

Local AI agents and CI jobs need different access models

Local AI agents and CI jobs need different access models because human approval, workload identity, and credential exposure create different risks.

Local AI agents and CI jobs need different access models

A developer workstation and a build runner may both execute shell commands, call APIs, and push code. Treating them as the same security environment is how teams end up putting a deployment credential in a place where neither a person nor a service can properly account for it.

Local AI agents operate in an attended environment. A person can see a prompt, inspect a diff, reject an unusual request, and revoke a process that has started behaving badly. CI jobs operate because an event occurred: a push, pull request, tag, scheduled run, or manual dispatch. The job needs machine-verifiable authority tied to that event. It cannot wait for a developer to bless every call, and it should not inherit a developer's standing access because the workflow happened to run.

The distinction is not academic. It decides whether an agent receives a secret, whether a token exists for minutes or months, what an audit entry must say, and whether a compromised dependency can turn into a production incident.

Human presence changes what an approval means

An approval on a workstation can be a meaningful security control because a person is present to judge the immediate action. The approval must describe the caller well enough for that judgment to be useful. “An agent wants to make an HTTP request” is poor evidence. “A process signed by this authority, launched in this session, wants to use the production deploy credential” gives the operator something concrete to accept or reject.

That model has a hard boundary: approval is about a human taking responsibility for a live process. It is not a substitute for identity. If a malicious process can impersonate the trusted caller, hide the destination, or reuse an approval after its task changes, the prompt has become theater.

On a developer machine, I want three separate facts:

  • the protected store remains locked until the local user unlocks it;
  • the first request from a new process requires a session decision;
  • sensitive credentials can require a decision for each use.

These controls answer different questions. The lock says whether any action may occur. Session authorization says whether this process may act during this run. Per-call approval says whether a particular credential is too consequential to reuse casually. Teams often collapse all three into a single “allow agent access” button, then discover that the button approved far more than the developer intended.

A local agent should also receive results, not credential material. If the agent needs to query an API, a component outside the agent can attach the credential, make the request, and return the response. That keeps a prompt injection in the agent from simply printing an API key into a terminal, a patch, or a chat transcript. Masking a secret after it has entered an agent context is not equivalent protection. The agent may encode it, send it to another host, or use it in a request before any masker sees it.

Sallyport uses this attended model on macOS: its vault stays behind an absolute gate, and it can require authorization for a process session or for every use of a selected credential. That makes sense when a developer is there. It would be the wrong primitive for an unattended runner.

A CI job needs an identity that survives scrutiny

A CI job cannot supply human intent on demand. It needs a workload identity, which is an identity derived from verifiable facts about the job rather than from a secret copied into the job environment.

For a deployment workflow, those facts usually include the CI issuer, repository or project, commit or ref, workflow identity, environment, and intended audience. The target service checks the signed assertion and exchanges it for a short-lived credential. This is the useful part of OIDC federation: the runner proves where this job came from without carrying a reusable cloud key in its secret store.

GitHub Actions documents this pattern through its OIDC token endpoint and id-token: write permission. The permission name is easy to misunderstand. It permits the workflow to request an identity token; it does not grant deployment permission by itself. The cloud role or target service must still reject tokens whose issuer, audience, subject, and other claims do not match the intended workflow.

That second half is where many configurations fail. A role that accepts any token from a repository has delegated too much authority to every qualifying workflow in that repository. A documentation preview, a release workflow, and a production deployment should not become equivalent merely because they share source control.

Use claims to make the role describe one job class. The exact claim syntax depends on the CI provider and cloud, but the policy should answer plain questions:

  • Which repository may request this role?
  • Which workflow file or protected environment may request it?
  • Which branch, tag, or release condition may request it?
  • What audience must the assertion name?
  • How long can the issued credential remain usable?

Do not write policy conditions that you have not inspected in a real token. Print the claims in a safe test environment, compare them to the trust policy, and test denial cases. People test successful deployments and leave the dangerous path, a pull request from an untrusted branch, as an assumption.

Secrets and identities solve different problems

A secret proves possession. An identity assertion makes a claim about the workload that requested access. Both may end in a bearer token, but they create very different failure paths.

A stored CI secret normally has no memory of why the job received it. If a workflow can read DEPLOY_TOKEN, then a changed script, compromised action, malicious pull-request path, or log command can use that token wherever the token's permissions allow. Rotation limits how long the secret remains useful, but it does not constrain the context of each use.

Short-lived federation does not make CI safe by magic. A compromised job can still use its valid token during the token lifetime. The gain is narrower blast radius: the attacker must run an eligible job, meet the issuer and claim rules, and act before the credential expires. You can also revoke or alter the accepting role without finding every copied secret.

Do not confuse short-lived credentials with low privilege. A token valid for ten minutes that can delete every production database is still unacceptable. Time limits reduce persistence; authorization scope limits damage. You need both.

Local agent access has the inverse concern. A developer may use the same local tool across many repositories and tasks, so a single broad API key can become an attractive target for a compromised agent. The safest local design keeps that key outside the agent and allows only the specific API call or SSH command the user has approved. If the external service supports fine-grained tokens, use them there too. A protected vault cannot improve an overpowered token after the request leaves the machine.

The dangerous convenience is one token for both worlds

Using a developer's personal token in CI is popular because it gets a blocked deployment moving quickly. It is also one of the worst ways to erase accountability.

A personal token often reaches farther than the pipeline needs. It may belong to an employee who changes teams, leaves the company, uses it from their laptop, and has access shaped by personal membership rather than deployment duties. When it appears in CI, an audit log can show that the token acted, but it cannot truthfully say whether the action came from the developer or a release job.

The reverse mistake also happens. Teams expose a CI deployment secret to a local agent so it can “test the same thing.” That gives interactive code generation an unattended production capability, often with fewer checks than the release workflow. It also makes incident response miserable: did the agent use the credential, did a shell script use it, or did a copied value leak into another tool?

Give each environment its own authorization boundary. A local developer may have interactive, revocable access to a development endpoint. A release job may receive a federated role limited to a protected production environment. A pull-request job may have no write authority at all. These are not inconveniences to smooth over. They are the evidence you use later to explain why an action was allowed.

A useful naming rule is that credential names should reveal both actor and purpose. ci-release-prod-deploy tells a reviewer far more than deploy-token. Better still, the CI job never stores a token with that name. It requests an identity-bound role whose trust policy encodes the same purpose.

Runner state turns temporary access into residue

Return API results, not secrets
Sallyport injects HTTP credentials itself, so an MCP-capable agent never holds the bearer token.

A job may use a temporary credential and still leave permanent trouble behind. The common leaks are logs, shell tracing, cached home directories, Docker layers, workspace artifacts, and files written by third-party actions.

Self-hosted runners deserve special suspicion because they can retain state between jobs. A job that checks out untrusted code can plant a modified executable, alter a shared cache, inspect a leftover workspace, or wait for a privileged workflow to reuse the host. Isolation by repository name does not help if jobs share the same operating-system account, container socket, or filesystem.

Ephemeral runners remove a large class of residue because the runner disappears after the job. They do not remove the need to control workflow inputs. A privileged job that runs scripts from an untrusted pull request is still handing authority to untrusted code, even on a fresh machine.

GitHub Actions warns that pull_request_target runs in the context of the base repository and can access secrets or write permissions. The event has a legitimate purpose: maintainers may need to label or comment on a pull request from a fork. The failure appears when a workflow triggered by that event checks out the pull request's head commit and executes its scripts. The workflow has now combined trusted credentials with attacker-controlled code.

Keep the pattern boring:

  • Run untrusted pull-request code without deployment authority.
  • Reserve protected environment access for reviewed refs and controlled workflow paths.
  • Pin third-party actions to immutable commit references where your process permits it.
  • Keep secret material out of caches, artifacts, and diagnostic output.
  • Destroy sensitive runner instances after the job.

The first and fourth items catch more real incidents than elaborate token naming conventions. A perfectly scoped credential still leaks if a shell command prints it or an artifact bundles its configuration file.

A deployment identity should be inspectable in one file

A workflow should make the request for outside authority visible. This GitHub Actions example requests an OIDC identity only in the deployment job and declares a production environment. It intentionally contains no stored cloud credential.

name: deploy

on:
  push:
    tags:
      - "v*"

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@<full-commit-sha>
      - name: Request deployment identity
        run: |
          token=$(curl -sS \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=deploy.example.internal")
          test -n "$token"
      - name: Deploy
        run: ./scripts/deploy.sh

The real exchange normally uses an action or cloud CLI rather than saving the response in a shell variable. The artifact here is the shape of the request: GitHub supplies a one-time request token and URL, the workflow asks for a specific audience, and the target then decides whether that identity may assume a role. The runner should never treat the resulting identity token as a general API credential.

Your cloud-side trust policy must reject a token unless its claims identify this release context. Do not copy a provider example and stop there. Provider examples often start broad because they must work for many users. Tighten the repository, environment, branch or tag, and audience constraints before the role can change production.

If a target cannot validate OIDC directly, put a small credential broker in front of it. The broker validates the CI assertion, maps claims to narrowly defined actions, issues a temporary target credential, and logs the mapping. Do not solve this limitation by placing a permanent target secret in every workflow that needs the service.

SSH exposes the difference especially clearly

Approve the process, not a token
A new agent process needs one session approval before Sallyport performs its first external action.

SSH keys make poor shared CI furniture. A private key copied into a CI secret can authenticate from any job allowed to read it, and its public half rarely tells the target which repository revision requested the connection. Forced commands, source restrictions, and separate accounts can limit the damage, but the basic identity remains a reusable private key.

For local work, SSH may need an interactive gate because a developer can see that an agent wants to connect to a particular host. The private key should stay in protected storage, and the agent should request a specific command rather than gain raw access to the key. The host must still enforce its own account permissions and command restrictions. Local approval controls the launch point; it does not make a dangerous remote command safe.

For CI, prefer short-lived SSH certificates when your SSH certificate authority and target fleet support them. The job uses workload federation to request a certificate with a short validity period, a restricted principal, and perhaps a forced command. The target verifies the certificate authority rather than accepting a copied private key forever.

If certificates are not available, use a distinct deploy account and a dedicated private key per deployment class. Restrict that account in authorized_keys and at the server. A minimal restriction looks like this:

command="/usr/local/bin/receive-release",no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA... ci-release

This line prevents the key from opening an arbitrary interactive shell and forces the server to run one receiver program. It does not identify the source repository by itself. Pair it with a brokered, short-lived key or another verified workload signal if the release decision depends on repository context.

Do not set StrictHostKeyChecking=no in a CI script to make SSH work. That disables the server identity check precisely where runners are easy places to redirect traffic. Supply known host keys through a controlled mechanism, rotate them deliberately, and fail a job when host verification changes unexpectedly.

Audit records must answer different questions

An action log that merely says “deployment succeeded” is operational output, not enough security evidence. You need to reconstruct who or what received authority, which request it made, and whether the record itself changed afterward.

For an attended local agent, record the local process identity, session start, approval decision, credential label or action class, destination, request time, and result. Do not record secret values or full sensitive payloads. A process signature or code-signing authority is more useful than an arbitrary process name, because names are cheap to copy.

For CI, record the CI provider, run ID, repository, workflow reference, commit SHA, actor that triggered the run where available, runner class, OIDC subject and audience, target role, action parameters that are safe to retain, and result. This lets an investigator distinguish a tagged release from a manually rerun job and a trusted workflow from an accidental broad grant.

Keep authorization logs separate from application logs. Application logs may be mutable, sampled, or deleted as part of normal operations. An authorization trail needs append-only behavior and independent verification. Hash chaining detects changes to the sequence when you retain the chain and verify it against expected records; it does not stop an attacker from preventing future records, so ship records off the compromised machine when the design allows it.

Sallyport projects session and individual action journals from a write-blind encrypted, hash-chained audit log, and sp audit verify checks the chain offline without a vault key. That is useful evidence for attended agent actions. CI should produce an equivalent workload-bound trail in the systems that issue and accept its identities.

Approval fatigue is a local design failure

Lock agent actions at the vault
The vault gate denies every action while locked, with Secure Enclave and Touch ID on supported Macs.

Per-call approval can protect high-impact local credentials, but a prompt on every harmless read trains people to accept prompts without reading them. Once that happens, the control becomes a ritual and an attacker only needs to wait for routine work.

Use per-call approval for actions whose consequences are hard to reverse: production writes, DNS changes, organization-wide administration, source-control force pushes, or commands against sensitive hosts. Use session authorization for repetitive low-risk actions that a developer can reasonably delegate during one agent run. Keep the vault gate separate so a locked machine denies all actions even if an old session approval exists.

CI has its own version of fatigue: manual approval gates that appear on every job and get approved because release timing has become the priority. A protected environment approval can be appropriate for a production deployment, but it should approve a clearly identified release artifact and target. It should not compensate for a workflow that accepts untrusted code, a role with broad claims, or a runner with unknown state.

A practical review asks one uncomfortable question: if this approval were clicked automatically for a month, what could run? For local agents, reduce the approved action surface until the answer is tolerable. For CI, remove the approval from ordinary machine actions and bind authority to the job's identity instead.

Separate the paths before the next credential request

When an agent or pipeline asks for access, classify the caller before choosing a secret mechanism. Is a person present and able to inspect the action? Is the caller a repeatable workload with claims you can validate? Can the caller receive a narrow result instead of a credential? Does the target permission match one named task?

If the caller is a local agent, protect credentials outside its context, retain a meaningful approval path, and log each action in a form a developer can review. If the caller is CI, use short-lived federation, bind roles to workflow claims, isolate the runner, and record the workload facts that authorized the call.

Do not let a shared token erase that boundary because it is faster today. The next incident will force you to rebuild it while trying to work out whether a person, an agent, or a runner actually acted.

FAQ

Why should AI coding agents and CI pipelines use different credentials?

A local agent runs inside a developer's working session, where a person can inspect a request and interrupt the process. A CI job runs unattended after an event in source control, so its permissions must come from a tightly scoped workload identity and expire with the job.

Can I use the same approval flow for CI and local agents?

No. A human approval prompt proves only that someone clicked at a moment in time; it does not describe the job's repository, commit, workflow, or intended cloud role. Use approval for interactive local work and workload-bound authorization for CI.

Are long-lived API keys ever acceptable in CI?

It is usually a poor trade. A long-lived secret turns any compromised runner, log leak, cache archive, or malicious dependency into a durable access path. Prefer short-lived tokens minted from an OIDC assertion with repository, ref, workflow, and audience constraints.

What is OIDC workload identity in CI?

An OIDC workload token is an assertion from the CI provider about the running job. A cloud or secret service validates that assertion and issues a short-lived credential for a specific role, rather than asking the job to carry a stored cloud secret.

Should a local AI agent ever see an API key?

They should receive no credentials at all if possible. Give the local agent an action interface that executes the request outside the agent and returns the result, while keeping the underlying API or SSH material in protected local storage.

How do I keep a CI job from accessing the wrong environment?

No. The workflow should state every external destination it needs, and each destination should have its own identity and permissions. A generic deployment token is convenient until a compromised documentation build can deploy production.

What is the biggest security risk with self-hosted CI runners?

Treat runner state as hostile residue. Use ephemeral runners for sensitive jobs, pin dependencies, avoid restoring secret-bearing directories, redact logs, and assume a privileged job can be tricked through its checked-out code.

How do I audit a CI credential?

First inspect the credential source. Then inspect the audience, repository or project claim, branch or environment constraint, lifetime, and target permissions. If you cannot explain each of those fields, the token has more trust than the job deserves.

What should an audit log record for agent actions?

Separate the records. An interactive session log should identify the local process and every approved action, while CI records should bind each action to the provider run ID, repository revision, workflow, runner class, and issued role. A plain success message is not an audit trail.

Can Sallyport run inside a Linux CI runner?

Use a managed cloud identity, a dedicated deployment broker, or a narrowly scoped service account behind short-lived federation. Sallyport is designed for the attended macOS side of this split, not as a headless CI credential service.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov