Version pinning for agent clients before sensitive access
Version pinning for agent clients lets teams test a known local build, verify its identity, and control when it can reach sensitive services.

A local agent client should not receive access to sensitive services merely because its name appears on an approved-tools list. The executable, its runtime, its extensions, and the way it starts determine what will make the request. If any of those change, you have a different security subject, even if the window title and command name look familiar.
Version pinning for agent clients is a release-control practice, not a cure for bad prompts or excessive permissions. It gives you one useful property: you can test a known artifact, record what you tested, and refuse to grant sensitive access to an unreviewed replacement. That sounds pedestrian. It prevents a surprising number of avoidable failures.
I have seen teams put serious effort into vaults and approval dialogs, then allow a desktop updater to replace the program making those requests overnight. They still had a human in the loop, but the human was approving behavior from code nobody had evaluated. That is not a meaningful boundary.
Pin the executable identity, not a friendly version label
An exact version number is necessary, but it does not identify a local client well enough on its own. A release label such as 2.4.1 tells you what the publisher intended to ship. It does not prove which bytes you installed, who signed them, what runtime launched them, or whether an extension changed their behavior after startup.
A usable approval record identifies the artifact at several layers:
- The client name and exact release string.
- The SHA-256 digest of the downloaded installer or executable.
- The code-signing authority and bundle identifier where the operating system exposes them.
- The install path, runtime version, and extension inventory.
- The date, reviewer, and the services or credential scopes approved for that record.
The distinction between a version pin and an artifact pin matters. A version pin tells an installer which release it should seek. An artifact pin lets you reject a file that differs from the file you tested. Teams often blur these terms because package managers use the word "pin" for both. The consequence is ugly: they approve 1.4.3, receive a rebuilt 1.4.3 from a compromised mirror or an altered cache, and have no local test that notices.
A signing identity adds another check, but it does not replace a digest. A legitimate publisher can sign a bad release, and a digest alone does not tell you whether the file came from the publisher you expected. Record both when the client has access to production systems.
Do not turn this into paperwork for its own sake. The record needs to answer a question during an incident: "Which code had permission to send that request?" If it only says "the coding agent," it cannot answer that question.
A locked dependency tree is different from a reviewed client
Lockfiles help, but they solve a narrower problem than many teams assume. A package lock selects dependency versions for a particular installation. It does not inspect native modules, verify every post-install action, prevent a runtime from loading code from a user directory, or establish that the launched executable is the artifact your reviewer examined.
This matters most for clients distributed through language ecosystems. A command that looks like one program may actually be a small launcher, a runtime, a package tree, and one or more downloaded plugins. Pinning only the top-level package can leave the largest part of the trust decision floating.
The OpenSSF SLSA specification draws a useful line between provenance and integrity. Provenance describes where and how an artifact was built. Integrity checks establish that an artifact has not changed. Neither claim means the program is safe to grant credentials. Both claims still matter because a team cannot review or reproduce a moving target.
Start by mapping what runs when a developer starts the agent. On macOS, ask the shell rather than trusting a dock icon:
command -v agent-client
file "$(command -v agent-client)"
head -n 1 "$(command -v agent-client)"
The head command matters when the first result is a script. A first line such as #!/usr/bin/env node tells you that the Node runtime and package tree are part of the execution path. A first line that delegates to another launcher means you need to continue tracing. Do not approve a shell alias, symlink, or bootstrap script as though it were the client.
For an application bundle, inspect the actual executable and signing metadata:
APP="/Applications/Agent Client.app"
BIN="$APP/Contents/MacOS/Agent Client"
shasum -a 256 "$BIN"
codesign -dv --verbose=4 "$APP" 2>&1 | grep -E 'Identifier=|TeamIdentifier=|Authority='
spctl --assess --type execute --verbose=4 "$APP"
The hash output has the shape digest path. The codesign output normally includes an identifier, a team identifier, and one or more authority lines. Save that output with the approval record. spctl asks macOS to assess the app under its current security policy. It is useful evidence, but it does not mean the app deserves access to a deployment credential.
A review that says "the lockfile is committed" leaves too much unanswered. Keep the lockfile. Then identify the artifact that actually initiates sensitive actions.
Protocol compatibility does not establish client trust
A client can speak an agent protocol correctly and still be unsuitable for sensitive access. Protocol compatibility answers whether two programs can exchange messages. Client trust answers whether this particular program, launched in this particular state, may ask for a credential-backed action.
The Model Context Protocol documentation describes a client and server exchanging declared capabilities during initialization. That capability negotiation is useful for interoperability. It is not an attestation of the client binary, a statement about loaded extensions, or a promise that the client will preserve user intent. Do not treat a successful handshake as an identity check.
This confusion shows up when teams allow any local MCP-capable client to invoke a sensitive tool because the tool has a known server name. The MCP server sees a request through a protocol channel. It may know little about the program that constructed the request. A malicious or simply unfamiliar client can use the same method names as a tested one.
Keep three records separate:
- The protocol version and capabilities you tested.
- The client artifact, signing identity, runtime, and extensions you approved.
- The service permissions that client may request.
A change in any one of those records deserves review. A protocol upgrade can alter defaults or message handling. A client upgrade can alter which tools it calls or when it retries. A service permission change can make a previously harmless retry destructive.
There is another distinction people get wrong: pinning a client does not pin an agent's instructions. The user prompt, repository files, tool descriptions, remote content, and model output can all influence a pinned client. Pinning limits surprise caused by software replacement. It does not make arbitrary tool use safe. Put a human approval point in front of actions whose consequences cannot be undone easily.
Test the permission path, not only the chat window
A candidate client passes review only after it behaves correctly across the access path you plan to allow. Asking it to summarize a repository or create a scratch file proves that the user interface works. It does not test credential injection, approval behavior, retries, redirects, SSH host checks, or what the client does after a tool returns an error.
Use a staging service or a purpose-built test credential with narrow scope. The credential should be able to demonstrate the action but unable to modify production data, rotate shared secrets, or access unrelated accounts. If you cannot create such a credential, the service is too coarse for an autonomous client and deserves its own access design discussion.
Test these cases deliberately:
- An ordinary permitted request with an expected response.
- A request that exceeds the credential's scope and receives a denial.
- A response delay or connection failure that triggers retry behavior.
- A redirect or changed endpoint, if the client uses HTTP.
- An SSH connection with a changed host key, if the client uses SSH.
The last two catch behavior that often surprises people. HTTP clients may follow redirects, and a redirect can send a request toward a different host. Whether credentials follow depends on the client and authentication implementation. You need to observe it, not infer it from a release note. SSH clients must treat a host-key mismatch as a stop condition until a person resolves the change. An agent should not get to decide that an unfamiliar host key is acceptable because the task asks it to continue.
Capture request metadata without recording secrets. For HTTP, a test endpoint can log method, host, path, status, and selected nonsecret headers. Confirm which authorization header the gateway adds, then confirm that the client never receives that header value back in a tool result, an error message, or a local transcript.
For a destructive endpoint, make the staging operation leave a recognizable marker. A test that merely returns HTTP 200 proves little. You want evidence that exactly one intended action occurred, that the wrong action did not occur after a retry, and that the audit record identifies the correct session.
Keep the candidate isolated from the approved client
A candidate release needs its own installation, configuration, caches, and extension directories. Sharing those directories gives the test build access to state that may differ from a clean install, and it makes rollback less credible. The most irritating failures I have debugged came from a "new" client quietly loading an old plugin or reusing an authenticated browser session.
On macOS, separate user accounts give the cleanest boundary for a serious test. A separate account changes the home directory, application support directories, caches, login items, and many credential stores. For a quicker developer test, separate directories can work if the client documents how to select each path and you verify that it honors them.
Do not point both versions at one writable configuration file. Clients frequently update configuration formats on launch. The newer build may write fields that the older one ignores or mishandles. That turns rollback into a partial migration, which is exactly when you need rollback to be boring.
Treat extensions with the same suspicion as the client. Record their exact versions, source locations, hashes where practical, and whether the client can fetch updates automatically. If the client discovers extensions from a broad directory such as a user-level plugins folder, the candidate test should begin with that directory empty. Add only the extensions required for the test.
A clean test also exposes a less glamorous issue: undocumented dependencies. If the candidate only works after it inherits environment variables, browser cookies, shell functions, or a global package cache from the approved setup, document those inputs. Every hidden dependency makes later behavior harder to reproduce.
Make promotion a small, repeatable release change
Promotion should replace one reviewed record with another, not rely on someone remembering which download button they clicked. Write down the same sequence every time. The sequence gives reviewers a shared vocabulary and gives the on-call engineer a sane rollback path.
- Download the candidate from the publisher's normal release channel and record its source, exact version, hash, and signer.
- Install it in the isolated test location and record the runtime and extensions it loads.
- Run the permission-path tests with staging credentials, including denied and failure cases.
- Compare the observed requests, prompts, and logs with the approved behavior. Investigate every new privilege request.
- Install the approved candidate in the privileged location, preserve the previous artifact, and change access only after the installation checks match the record.
That final order matters. Do not grant access first and plan to inspect the installed file later. If the installation check fails, leave the candidate unable to call sensitive services. A rejected release is a routine outcome, not a failed process.
Use a plain record that can live beside your operational notes. YAML works because people can read it during an incident:
client:
name: agent-client
version: "2.4.1"
executable_sha256: "replace-with-verified-digest"
signer_team_id: "record-the-observed-team-id"
install_path: "/Applications/Agent Client.app"
runtime: "native bundle"
review:
tested_on: "2025-03-08"
reviewer: "initials"
extensions: []
access:
environments: ["staging", "production-read"]
forbidden_actions: ["secret-rotation", "deployment-write"]
rollback:
previous_version: "2.4.0"
The values above are placeholders, not a template to copy as evidence. Fill them from your actual inspection. In particular, never paste a digest from a release announcement without hashing the file you downloaded.
Avoid broad rules such as "all releases from this vendor are allowed." They are popular because they reduce review work. They also erase the exact control that pinning provides. A vendor identity can be one input to review, but it is not a standing authorization for unknown code to use your production credentials.
Automatic updates and sensitive access should not share a boundary
Automatic updates are sensible for many desktop applications. The risk changes when the application can cause external actions through stored credentials, SSH keys, or privileged service accounts. In that situation, an updater can change the program that requests authorization between one workday and the next.
You have three workable patterns. The safest is to disable automatic replacement for the privileged installation and promote releases manually. Another is to let developers use automatically updated copies that have no sensitive access, while a pinned copy handles privileged work. A third is to put the action boundary outside the client so that every new process must receive fresh authorization before it can do anything consequential.
The third pattern limits damage from a surprise update, but do not overstate it. A fresh approval card is only useful if it identifies the process meaningfully. Seeing a generic client name and clicking through it every morning trains people to approve whatever appears. Show the signing authority, process path, or other identity evidence that a reviewer can compare with the approved record.
Do not silently allow an updated client because it came from an operating system app store or because macOS considers it signed. Those mechanisms reduce some supply-chain risks. They do not say whether the new behavior matches your access rules. Your organization still owns the decision to grant its credentials.
Version pinning also needs an expiry habit. A permanent pin becomes an unpatched pin. Set a review cadence based on the client, the services it can touch, and the vendor's security advisories. The review does not need drama when behavior has not changed. It does need a deliberate decision.
Per-action authorization catches the behavior a pin cannot
A pinned client can still receive malicious instructions through a repository, an issue description, a web page, or a tool result. It can also make a bad choice within permissions that you granted for legitimate work. Human authorization should focus on actions with material effects, such as writing production data, changing infrastructure, transferring data to a new destination, or opening an SSH session to a sensitive host.
Approval prompts fail when they ask people to approve technical noise. A prompt that appears for every harmless read trains users to click. A prompt that appears after the request has already obscured its target does not help much either. Show the action, destination, method, and credential identity before the gateway sends the request. Keep the number of choices small enough that a person can actually assess them.
A practical split is to allow low-risk reads through a session approval and require an explicit approval for actions that can change state or expose data. The boundary depends on the service. A read against a source repository may be ordinary. A read against a customer database may be data disclosure. Do not classify by HTTP verb alone.
Sallyport uses a fixed decision ladder: the vault denies every action while locked, a new agent process requires session authorization by default, and selected credential entries can require approval on every use. That narrow model is intentional. A general policy language would offer more knobs, and it would also give teams more ways to accidentally write an exception they do not understand.
Make revocation immediate. When a candidate client acts unexpectedly, you should be able to stop its current access session before you begin a long investigation. Revoking the service credential may be necessary later, but it is a blunt response that can break unrelated work. Session revocation contains the running process first.
Audit records must connect an action to the approved run
An action log that says "API call succeeded" is not enough for agent work. You need to connect the call to the local process, the approval decision, the credential used, and the client record that was current at the time. Otherwise an incident review will collapse into matching timestamps across terminals, browser history, and service logs.
Keep a session record for each agent process. It should include when the process started, how the user authorized it, its observed identity, and when it ended or was revoked. Keep a separate activity record for individual actions. That record should identify the destination, operation, outcome, and session association while omitting secret material.
Tamper evidence matters because an agent can generate a large volume of work in a short period, and local logs are easy to edit after a mistake. A hash chain makes deletion or alteration detectable when someone verifies the sequence. It does not prevent a compromised machine from taking actions. It gives investigators a better basis to detect a rewritten history.
Sallyport projects both its Sessions and Activity journals from one encrypted, hash-chained audit log, and sp audit verify can check the chain offline over ciphertext without a vault key. That design is useful when the person reviewing the trail should not receive access to the secrets used in the actions.
Test your audit path during promotion. Approve a staging session, perform one allowed request and one denied request, revoke the session, then verify that the records identify all four events. If your records cannot show the denied request or the revoke, you are missing information that becomes important precisely when behavior goes wrong.
Pinning fails when the surrounding system stays mutable
A carefully hashed client still runs on a machine that can change beneath it. The operating system, runtime, shell environment, DNS configuration, proxy settings, certificate stores, local helper binaries, and developer-installed extensions all affect how requests leave the machine. Pinning is one control in a chain, not a label you paste onto a risky setup.
Start with the parts that can alter privileged actions without changing the client digest. Inspect environment variables used to select endpoints, proxy behavior, or credential locations. Check which SSH binary or helper the client invokes. Record the intended known-hosts location and confirm that the test rejects an unfamiliar server identity. Review whether configuration permits arbitrary local command execution as a tool.
Do not solve every uncertainty by building a policy engine. Most teams need fewer moving parts, not a large rule set that nobody can explain at 2 a.m. A short allowlist of destinations, limited credentials, explicit approvals for sensitive calls, and a known client artifact cover more ground than an elaborate collection of untested conditions.
The operational habit is simple: when a client changes, access pauses until the new artifact earns it. Keep the old approved copy available, test the replacement against the real permission path, and record the evidence. That discipline is less exciting than autonomous demos. It is also how you prevent a background update from becoming an unreviewed production change.
FAQ
Should I pin an AI agent client to an exact version or a version range?
Pin the exact build that will request access, not only its major or minor version. A range such as ^1.8.0 gives a package manager permission to choose a different release later, which defeats the point of a review.
Is a lockfile enough to secure an agent client?
No. A lockfile freezes resolved dependencies, but it does not prove that the installed executable matches the reviewed artifact. Keep the lockfile, then verify the package hash, signature, or source commit that produced the client you permit.
What should I do if an agent update has a different code signature?
Treat a changed code-signing identity as a new client, even when the version string did not change. The signing authority tells you who signed the executable; the version tells you what the vendor claims it is. You need both records.
Does version pinning make autonomous agents safe?
Pinning reduces surprise from ordinary updates. It does not stop an approved build from following hostile instructions, abusing its permitted credential, or loading an unsafe extension. Keep human authorization and narrow credentials in place.
Which part of an AI agent setup should be pinned?
Pin the local binary that opens connections or invokes the MCP server, plus any managed runtime and extensions that execute in its process. If a launcher downloads the real client at startup, pinning the launcher alone gives you very little control.
How do I test a new agent version before giving it production access?
Test the full action path with a credential that cannot harm production. Exercise denied calls, allowed calls, approval prompts, redirects, malformed responses, and SSH host verification. A successful prompt-and-response demo says almost nothing about privilege behavior.
Can I run a candidate agent release beside the approved version?
Use a separate installation directory or separate macOS user for the candidate build. Keep its configuration, caches, plugins, and credentials separate from the approved client, or your test can quietly reuse production state.
Should I disable auto-updates for local AI coding agents?
Automatic updates are acceptable only when the updated client cannot reach sensitive services until someone promotes it. Disable automatic replacement for the privileged copy, or put the privileged copy behind an authorization point that recognizes the approved build.
What information belongs in an agent client approval record?
Record the client name, exact version, artifact digest, signing authority, install path, runtime version, extension inventory, test date, reviewer, and permitted scopes. Without the digest and signer, a version number is mostly a label.
How can I audit which agent version used a credential?
A useful audit trail records both the client run and each sensitive action, then lets you revoke a running session. Sallyport keeps session and activity journals from a write-blind encrypted, hash-chained audit log, which makes it practical to tie access decisions back to a specific approved run.