8 min read

How should unsigned local builds earn approval?

Unsigned local builds can work safely when approval follows process provenance, session boundaries, and scoped credentials instead of blanket trust.

How should unsigned local builds earn approval?

An unsigned local build is not automatically suspicious. It is also not a recognizable identity. Those two facts have to coexist, or your approval flow turns into one of two bad habits: developers approve every anonymous process because they need to work, or they give up and run agents with direct credentials.

The useful question is narrower: what evidence lets a human approve this particular run of a locally compiled agent without pretending that every process from a familiar folder deserves the same trust? The answer is a workflow built around session boundaries, launch provenance, credential scope, and a willingness to deny requests that cannot explain themselves.

Unsigned is a missing claim, not a risk rating

Unsigned code has not made a publisher-backed identity claim that you can verify through a certificate chain. That says nothing by itself about whether the code is malicious, reviewed, locally modified, or freshly built from the repository you have open. It only removes one kind of evidence.

Teams get this wrong in both directions. One group treats any unsigned executable as hostile, which pushes normal development into side channels and personal API keys. Another group treats unsigned as shorthand for "my local code," which creates a broad approval category that an unrelated process can exploit.

Keep these distinctions separate:

  • Publisher identity answers who signed a distributed build.
  • Build provenance answers which source, revision, machine, and command produced this executable.
  • Runtime provenance answers what launched the process requesting access right now.
  • Action authority answers what credential or remote access that process may use.

A signature can help with the first question. A clean local workflow must answer the other three. If a developer compiles an agent client from a worktree, their confidence usually comes from the repository state and the command they just ran, not from a public certificate. The approval experience should expose that evidence instead of treating the absence of a certificate as a verdict.

Apple's code-signing documentation makes the same boundary clear in a more formal way. A designated requirement identifies an instance of signed code under the policy that checks it; it does not prove that the code is appropriate for every resource or every future action. Apple also notes that each macOS subsystem applies its own trust policy. An action gateway should therefore avoid turning code-signing status into a universal permission decision.

A local-build lane needs evidence that another process cannot borrow

Give locally compiled agent clients their own approval lane. Do not fold them into a bucket named "unsigned," "development," or "terminal." Those labels describe too many processes to be useful.

The lane should ask for four pieces of evidence before a developer approves a session:

  1. The executable path must sit inside a developer-controlled checkout or build output directory.
  2. The parent process must be an expected launcher, usually a terminal, IDE task, or a wrapper script that the team owns.
  3. The repository state and build command must be easy to inspect without reconstructing the day from memory.
  4. The requested destination and credential must match the work underway.

None of those signals is perfect alone. Together, they make it hard for a random download, background helper, or compromised dependency to pass as a routine local run.

This is where teams often reach for a policy engine: allow paths under a directory, permit commands whose name begins with agent, exempt anything launched by a certain IDE. Do not do that. A static rule is attractive because it removes interruptions, but it turns an easy-to-review decision into a long-lived exception. Any process that can arrange the right path or parent chain inherits the exception.

Use human approval at the session boundary instead. The approval card should show the process's code-signing authority when it has one, but an unsigned build needs other context beside that field: executable path, parent command, working directory when available, and the target it intends to reach. The developer can then answer a real question: "Is this the client I just built for this task?"

Sallyport's per-session authorization fits this lane because it asks once for a new agent process and keeps that approval only until the process exits. The process identity is the decision unit, not a vague class of unsigned software.

A familiar pathname is evidence, not identity

A path like ~/src/agent-client/dist/agent feels reassuring because it tells a plausible story. It is not an identity boundary. A malicious process can run from that path if it can write there, replace an output, change a symlink, or persuade a launcher to resolve a different executable.

Start by making the legitimate story boring and repeatable. Each developer should own a normal checkout location. Build output should stay inside that checkout or in a predictable directory that only the developer's account can write. Avoid shared build directories, Downloads, temporary directories, synced folders, and project roots where package scripts routinely rewrite executables.

A practical launch contract can be as small as this:

#!/bin/zsh
set -eu

repo="$HOME/src/agent-client"
cd "$repo"

git status --short
git rev-parse --short HEAD
exec ./build/agent-client --mcp

The useful part is not the shell syntax. It is the evidence it leaves behind. The parent shell has a known script path, the working directory points to a checkout, the Git revision prints before the client begins, and exec replaces the shell with the intended program rather than leaving a confusing process chain.

Do not let this script download a release, choose a branch from an environment variable, run a package manager hook, or consult a mutable configuration file outside the checkout. Those conveniences make the launch contract less useful because the approval reviewer can no longer tell what the script actually selected.

If the agent client needs generated code, generate it as part of the explicit build command. If it needs a local configuration file, pass the file path visibly and keep it outside the repository only when that file contains machine-specific settings. Do not smuggle credentials into it. The gateway exists so the client does not need them.

Inspect the process before you approve the session

A good approval decision takes seconds, but it should not rely on recollection. When a new local process asks to call an API or open SSH, inspect its executable and parent chain before you click approve.

On macOS, these commands provide a useful first pass. Replace the sample PID with the PID shown by your process viewer or terminal:

pid=48271

ps -o pid=,ppid=,user=,lstart=,command= -p "$pid"
ps -o pid=,ppid=,command= -p "$(ps -o ppid= -p "$pid" | tr -d ' ')"
lsof -a -p "$pid" -d cwd -Fn

The output shape should look like this:

48271 47990 alex Tue Jul 22 10:14:03 2026 /Users/alex/src/agent-client/build/agent-client --mcp
47990 23112 /bin/zsh /Users/alex/src/agent-client/scripts/run-local-agent
n/Users/alex/src/agent-client

You are checking a chain, not collecting trivia. The binary should be in the expected build location. The parent should be your known launcher. The current directory should fit the checkout. A process that comes from /private/var/folders, ~/Downloads, an unfamiliar package cache, or a wrapper you did not expect has failed the review even if its command name looks correct.

Then inspect the executable itself:

client="$HOME/src/agent-client/build/agent-client"
file "$client"
codesign --display --verbose=4 "$client" 2>&1 | sed -n '1,18p'
shasum -a 256 "$client"

An unsigned binary may cause codesign to report that it is not signed at all. An ad hoc signed binary reports a signature without a public signing authority. That is still useful information, but do not read it as a team identity. Apple describes ad hoc signing as "Sign to Run Locally" and explains that its designated requirement is tied to that specific code version. A rebuild changes the evidence, which is exactly why session approval should not survive process replacement.

The hash is a comparison tool, not a trust signal. It helps when two developers need to establish that they ran the same output from the same revision. It does not make a binary safe because it has 64 hexadecimal characters beside it.

A developer should deny the request if any part of the chain is surprising. Do not approve first and investigate after the API call. Approval is the moment when uncertainty should cost a few minutes, not an incident review.

Use a session as the boundary between one build and the next

Trace calls back to sessions
Activity records individual HTTP and SSH calls, tied to the same encrypted audit log as agent sessions.

The session boundary solves a problem that path allowlists cannot solve: local code changes constantly. A developer may rebuild the client ten times in an hour. Each output can have a different dependency graph, different command handling, or a temporary debug branch that sends requests somewhere unexpected.

Make the launch process disposable. Start the agent client for one task, approve that process after review, and let the approval expire when it exits. When the developer rebuilds, changes branches, modifies the wrapper, or restarts the client, they get a new process and a new approval decision.

This sounds inconvenient only when the session is poorly defined. If an agent client starts and stops for each individual tool call, fix the client lifecycle or use a purposeful local supervisor that remains transparent in the process tree. Do not solve churn by making approval permanent. A short-lived process is supposed to be short-lived.

The workflow below works well for a developer running a locally compiled coding agent against a test environment:

  1. Build from the checkout and print the revision plus any uncommitted changes.
  2. Launch through the checked-in wrapper script.
  3. Confirm the displayed process path, parent command, and target service at the first authorization request.
  4. Approve the session if those details match the task.
  5. Exit the client when the task ends, then rebuild or relaunch for the next distinct task.

That workflow gives developers a clean recovery path. If a build is questionable, stop it. There is no hidden allowance to untangle and no rules file that has silently accreted exceptions for half the team.

Do not confuse a session approval with approval for a repository. A repository can be clean while the launch command is wrong. A launch command can be correct while the branch is experimental. A session decision says that this process, with this context, may make ordinary calls for the current run.

Reserve per-call approval for consequences that cannot be undone

Session approval is the right default for repeatable development traffic: reading issue metadata, querying a sandbox API, fetching a repository over SSH, or updating a disposable test record. Requiring a human gesture on every one of those calls teaches people to approve without reading.

Some credentials should still require approval on every use. Choose them based on the consequence of the remote action, not the sensitivity theater around the secret itself.

Use per-call approval for credentials that can:

  • write to a production environment;
  • publish a package, release, or deployment artifact;
  • access a customer data export or another concentrated sensitive dataset;
  • alter organization membership, authentication settings, or recovery controls;
  • reach a production SSH host.

Do not mark every development API token this way. That creates approval fatigue, and approval fatigue converts a careful reviewer into a button operator. The gate should make dangerous moments distinct enough that people notice them.

The request detail matters as much as the second prompt. For HTTP, show the method and destination, then enough of the path to identify the action without dumping sensitive request content into an approval surface. GET /v1/test-runs/123 and DELETE /v1/projects/123 should never look interchangeable. For SSH, show the host and account, and make the developer confirm why that host belongs in the current task.

A per-call requirement is also a useful brake on a locally modified client. You may be comfortable approving a session for a new experimental build that reads from a staging service. You should still hesitate before that same build can publish a production release. That hesitation is the point.

Treat rebuilds, branch switches, and wrappers as new evidence

Approve the run, not unsigned code
Sallyport approves a new agent process once, then ends that approval when the process exits.

Developers often make one approval decision in the morning and then change the facts all day. They pull a branch, run a code generator, update dependencies, alter a prompt file, add a shell alias, or replace a wrapper script. The binary may retain the same name and path while its behavior changes materially.

Adopt a simple rule: if the executable, its launcher, or its intended environment changes, end the session and relaunch. You do not need a formal ceremony for every edit. You need the discipline to avoid carrying yesterday's context into a new build.

There are three events that deserve an automatic pause in your own workflow:

  • You changed the branch, commit, dependency lockfile, or generated output.
  • You changed the script that launches the client or the environment variables that shape its behavior.
  • The agent now wants a different API host, a different SSH host, or a credential with broader consequences.

A branch switch gets underestimated because the binary name stays stable. Yet a feature branch may contain an experimental integration, a modified tool manifest, or a debugging endpoint. The right response is not to ban branches. It is to make the first request from the new run visible and reviewable.

Environment variables deserve the same suspicion as executable arguments. A client started with API_BASE_URL, SSH_AUTH_SOCK, PATH, DYLD_*, or a custom configuration location can behave differently from the source you inspected. Your wrapper should set only the variables the client needs, print non-secret values that affect routing, and reject missing values rather than sourcing a sprawling shell profile.

For example, this is reviewable:

export AGENT_ENVIRONMENT=staging
export AGENT_API_ORIGIN=https://staging.example.internal
exec ./build/agent-client --mcp

This is not:

source "$HOME/.agent-env"
eval "$(tooling configure-agent)"
exec "$AGENT_BIN" "$@"

The second form may be convenient, but it hides the executable, configuration source, arguments, and side effects. It forces the person approving the process to trust a pile of indirection. Local development has enough moving parts already.

Keep credentials outside the client, even when the client is yours

A locally compiled client is easiest to trust when it never receives the credential it wants to use. If the client reads an API token from an environment variable or SSH key from a file, it can print it, forward it, cache it, include it in a crash report, or hand it to a subprocess. The developer's confidence in their own build does not change that exposure.

Put the secret in the action gateway and let the client request an action by reference. The client should provide the HTTP request or SSH command context it needs to perform work. The gateway injects the credential, performs the call, and returns the result. This keeps the approval decision focused on the action rather than on whether the client may possess a bearer token indefinitely.

For HTTP, establish separate credentials for separate environments and purposes. A staging token should not route to production merely because a client supplied a different host. For SSH, use distinct host entries or credential records for distinct roles. Do not rely on a single all-powerful key plus a human promise to select the correct target.

Sallyport keeps API and SSH credentials in an encrypted vault and executes the action itself, so an agent receives results rather than plaintext secrets. That design matters most for local builds, where source changes are expected and a leaked environment variable is otherwise only one debugging print away.

The separation also improves incident response. If a client behaves oddly, you can stop or revoke its session without rotating every secret it might have loaded into memory. The vault gate remains absolute while locked, and a locked gateway denies actions rather than trying to infer the developer's intent.

The audit trail should settle disagreements, not merely collect events

Keep local builds credential-free
API keys and SSH keys stay in Sallyport's encrypted vault, never in the locally built agent.

When a local-build approval flow works, people will occasionally ask why an agent accessed a service or whether a process was still running after a handoff. The answer should come from an event trail, not from guesses, terminal history, or a chat message written after the fact.

Record two views of the same underlying activity. One should show agent runs and support immediate revocation. The other should show individual HTTP and SSH actions. Keep the evidence tied together so a reviewer can move from a suspicious request to the session that authorized it and then to the process context that caused the decision.

A tamper-evident log is particularly useful here because local builds can be modified by the same people reviewing them. Sallyport projects its Sessions and Activity journals from one encrypted, hash-chained audit log, and sp audit verify checks that chain offline over ciphertext without requiring a vault key. That gives a team an integrity check even when the reviewer should not receive access to the secrets behind the actions.

Run the verification as part of a review or incident drill:

sp audit verify

The expected outcome is a clear success or a report that identifies a chain problem. Treat a verification failure as an operational issue until you understand it. Do not delete the journal, reinstall the app, or accept a fresh baseline before preserving the affected records.

The journal does not replace review at approval time. It gives you a way to test whether your process is producing evidence you can actually use. If the log says only that "an unsigned client" made a call, improve your launch contract and approval context. If it shows the process, session, destination, time, and action, the team can investigate without turning every developer's machine into a forensic project.

Team conventions prevent approval prompts from becoming background noise

The hardest part of a local-build approval workflow is not the command line. It is keeping the human meaning of an approval intact after months of normal work.

Write down a small local-agent convention and keep it near the repository, not buried in a security handbook. It should state where supported checkouts live, what launcher scripts look like, which environments count as ordinary development, which credentials require per-call approval, and what developers do when the prompt shows an unexpected process.

Make the deny path normal. A developer who clicks deny because a parent process looks odd should not feel like they blocked progress. They should stop the process, inspect it, relaunch through the known wrapper, and approve the clean run. That is faster than normalizing a mystery process and trying to explain it later.

Review the convention after a real surprise: a package script rewrote an output, an IDE launched a helper you did not expect, a branch pointed at the wrong environment, or an agent requested a credential outside its task. Those failures are useful because they reveal which evidence was missing. Do not respond by adding a permanent allow rule. Tighten the launch contract, improve what the approval surface shows, or narrow the credential.

A developer who compiles their own agent should not have to choose between security theater and constant interruption. Give them a session approval that is specific to a visible process, keep consequential calls behind a separate decision, and require a fresh review whenever the local build story changes. That is strict enough to catch the wrong process and practical enough that people will keep using it.

FAQ

Are unsigned local builds safe to approve?

No. An unsigned build tells you that a public signing authority is not vouching for it; it does not tell you who compiled it, where it came from, or why it is running. Treat it as a request for a local trust decision, then require evidence that connects the process to a developer, a checkout, and a narrow purpose.

Should I approve every locally compiled agent once or every time it runs?

A local build should receive a fresh session approval when the prior client process exits. That gives the developer room to iterate without turning a one-time click into permission for unrelated processes later in the day.

How can I tell whether a local agent came from my own source tree?

Use a stable developer-owned checkout and a documented launch command, then inspect the executable path and parent process before approving it. A directory name by itself proves almost nothing, because any process can run from a directory with a reassuring name.

Does ad hoc signing make a locally built agent trustworthy?

Ad hoc signing gives macOS a code identity tied to that build, but it is not a publisher identity. It can make accidental substitution easier to spot, yet it should not replace session approval, scoped credentials, and activity review. Apple notes that an ad hoc signature's designated requirement is tied to that specific version of code. (developer.apple.com)

Can I trust any process that has the same local build name?

Do not approve it as a broad category. Inspect the current process and approve that run only when its source location, parent command, intended repository, and requested action make sense together. A new process gets a new decision.

When should an unsigned build require approval for every call?

Use session approval for ordinary development calls, then mark credentials with per-call approval when a single request can alter production, publish code, move money, or expose sensitive records. The second prompt should protect the dangerous use, not compensate for weak process identification.

What should I do if I approved the wrong local agent process?

Stop the agent process, revoke the active session if the gateway offers that control, and review the individual actions before you restart anything. Do not attempt to reason backward from a terminal scrollback after the agent has continued running.

Can a team share one unsigned agent build directory?

A shared build folder is a poor boundary because another person, script, package manager, or sync client can replace files there. Each developer should build from a checkout they control and start the agent through a command that leaves a visible parent-process trail.

Is it safe to launch a local agent through a shell script?

A normal shell script is fine when it only sets known paths and starts the expected executable. It becomes a problem when it fetches code, selects a branch, expands an unreviewed environment variable, or quietly launches another binary before the agent starts.

Does locking my Mac reset trust for a local agent?

No. The gate should deny actions while locked, and a new agent process should still need its own session decision after the Mac is unlocked. Screen locking protects the device; it does not identify the next process that asks to use a credential.

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