ALPS 0.1: agent signing isolation
Start with proof in minutes. Add observable, enforceable isolation as your repository risk grows.
On this page
Choose an ALPS level
Agentic Levels for Provenance and Signing is a usability–assurance ladder, not a pass/fail grade. Choose a level for the repository’s risk and environment. Each level is cumulative, reduces reliance on model behavior, and preserves the same in-toto evidence and policy semantics.
Requirements are cumulative: each row includes every requirement above it.
| Level | Minimum requirements | Verifiable evidence | Focus |
|---|---|---|---|
| ALPS 0 · Guided | Agent/repository guidance and CI/lock evidence for the exact command, result, and commit. | Signed statement bytes, subjects, command/result, certificate, and trusted time; no independently verified signer separation. | Quick adoption and accidental misuse. |
| ALPS 1 · Authenticated | Platform-issued tenant, repository, purpose, audience, and time-bound identity; mandatory RFC 3161 timestamp. | Authenticated workload issuance and ephemeral signing event bound to the evidence. | Impersonation, replay, and stale-proof ambiguity. |
| ALPS 2 · Constrained | Enforced sandbox plus trusted observations of the measured agent, exact CI/lock executable, applied policy, process, environment, mounts, and egress. | Signed boundary facts showing the agent lacked named session, key, and socket paths within the observer’s coverage. | Direct credential access and common escape paths. |
| ALPS 3 · Isolated | Signing service outside the agent boundary; typed requests; measured/signed CI/lock identity; hardware-backed or remote-protected authority. | Independent request re-derivation, service identity, measured binary, and non-exportability evidence. | Compromised or adversarial agents. |
Hermeticity modifiers
Hermeticity is an orthogonal evidence facet. A high ALPS level does not imply a closed execution, and a lower ALPS level can receive a hermeticity modifier when trusted observations support it.
| Modifier | Open | Constrained | Complete | Evidence required |
|---|---|---|---|---|
| Hermeticity | H-Open Unrestricted external runtime influence is permitted and recorded. | H-Constrained External access is technically restricted to an explicit allowlist; destinations and relevant inputs are observed. | H-Complete All inputs are staged and content-identified; network is disabled or confined entirely within the independently measured boundary; no unmeasured external influence crosses it. | Signed facts covering the assessed boundary identity and scope, material inputs, ambient environment and descriptors, filesystem roots, toolchain, cache, network policy and outcomes, and explicit host inputs. |
ALPS-0 / H-OpenALPS-2 / H-ConstrainedALPS-3 / H-Complete
Hermeticity: Unknown receives no H modifier when evidence is missing or the observer’s boundary coverage is insufficient. Unknown is not treated as open.
ALPS 0.1 borrows its cumulative page structure from the SLSA v1.2 Build track. ALPS 0.1 measures agent-to-signer provenance and isolation; it is not a SLSA level, replacement, certification, or equivalence claim. SLSA Build L3 does not require hermeticity, and its hermetic-build discussion treats closed dependencies as a distinct assurance. The H-* modifiers are defined by ALPS 0.1, not SLSA.
Under ALPS 0.1, the producer does not self-assert a level. A verifier derives the highest supported level from signed evidence. A configuration file proves configured intent only; it becomes enforcement evidence when a trusted observer outside the agent records that exact policy digest as applied to the measured process. Missing evidence means Unknown, never safe by assumption.
Intent not independently proved.
Within the observer’s stated coverage.
External enforcement with signed measurement.
Cryptography and required bindings validated.
Never promoted to safe by assumption.
Where an assessed ALPS level would be consumed
Pushgate verifies signed evidence, identity, policy, and the platform decision at git push. It does not read, derive, or require an ALPS level, and no shipped verifier does. This section is the consumption contract an assessor would have to satisfy first, written down so an integration is designed against it rather than around it.
Git push is Pushgate’s first consumption-time enforcement point, and it is not a limit of ALPS: the same contract would let a verifier take independently assessed ALPS evidence as one policy input before granting any sensitive action — running a deployment, publishing an artifact, invoking a privileged API, changing infrastructure, or releasing access to a protected service.
Each authorization would remain narrow. A consumer must verify the ALPS attestation and bind its decision to the exact requested action, target, tenant, workload, inputs, purpose, and time window. An ALPS level is evidence for policy; it is never a reusable bearer credential or blanket permission to execute commands.
| Enforcement point | Example exact bindings |
|---|---|
| Git admission | Repository, commit, ref update, policy, and push nonce. |
| CI/CD deployment | Workflow, immutable artifact digest, environment, deployment command, and release approval. |
| Privileged API access | API audience, operation, resource, tenant, purpose, and short expiry. |
| Artifact publication | Registry, package identity, artifact digest, provenance policy, and destination. |
ALPS 0 · Guided
Mermaid source
flowchart LR
R[Agent rules] --> A[Coding agent]
A -->|invokes| C[CI/lock]
C --> E[Signed evidence]
N[Cooperative guidance] -. same process authority .-> ASummary
CI/lock records and signs the exact command, result, subjects, and commit while repository rules tell the agent how to use it.
Intended for
Teams that want a minutes-to-first-proof path for cooperative, safety-aligned coding agents without sandbox setup.
Requirements
Add the same rule to AGENTS.md, CLAUDE.md, and a project Cursor rule. Keep the real home, platform session, SSH agent, signer caches, cloud credentials, and signer sockets out of the agent environment.
# Agent proof rule
- Run each policy-bearing command through the approved, version-pinned CI/lock binary.
- Use repository setup commands exactly; do not add signer or platform flags.
- Never inspect or use platform sessions, signing authority, SSH_AUTH_SOCK,
signer caches, cloud credentials, or signer sockets.
- Report a CI/lock refusal to the human; never bypass it.
Evidence a verifier checks
Exact evidence bytes and subjects, observed command and result, signer certificate, commit binding, and RFC 3161 signing time. Instruction-file digests may be recorded as declared context.
Benefits and mitigations
Reduces accidental or casual credential misuse and makes the required proof workflow explicit.
Limits
There is no independently verified process separation or proof that credentials were absent. A prompt-injected or malicious same-user agent can ignore prose, read available files, environment, or sockets, select another binary, or run another allowed command.
Install and verify CI/lock
Install the supported release into a host-owned path outside the workspace. Pin the approved version and verify the signed installer before execution; the installer then verifies the release archive signature and published SHA-256 before installing it.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the cosign verification and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the cosign verification and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
if pushgate_host_has brew; then
PUSHGATE_BREW="$(pushgate_host_tool brew)" || exit 1
PUSHGATE_PREFIX="$("$PUSHGATE_BREW" --prefix)"
else PUSHGATE_PREFIX='/opt/homebrew'; fi
case "$PUSHGATE_PREFIX" in
"$PWD"/*) echo 'refusing: the installation prefix resolves inside the workspace' >&2; exit 1 ;;
/*) ;;
*) echo 'refusing: the installation prefix is not an absolute path' >&2; exit 1 ;;
esac
PUSHGATE_CURL="$(pushgate_host_tool curl)" || exit 1
PUSHGATE_COSIGN="$(pushgate_host_tool cosign)" || exit 1
PUSHGATE_BASH="$(pushgate_host_tool bash)" || exit 1
CILOCK_STAGE="$(pushgate_host_stage)" \
&& "$PUSHGATE_CURL" -fsSL https://cilock.dev/install.sh -o "$CILOCK_STAGE/install.sh" \
&& "$PUSHGATE_CURL" -fsSL https://cilock.dev/install.sh.sig -o "$CILOCK_STAGE/install.sh.sig" \
&& "$PUSHGATE_CURL" -fsSL https://cilock.dev/install.sh.cert -o "$CILOCK_STAGE/install.sh.cert" \
&& "$PUSHGATE_COSIGN" verify-blob \
--certificate "$CILOCK_STAGE/install.sh.cert" \
--signature "$CILOCK_STAGE/install.sh.sig" \
--certificate-identity-regexp '^https://github\.com/aflock-ai/rookery/\.github/workflows/release\.yml@.+' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$CILOCK_STAGE/install.sh" \
&& CILOCK_VERSION=v3.1.3 CILOCK_BIN_DIR="$PUSHGATE_PREFIX/bin" \
"$PUSHGATE_BASH" "$CILOCK_STAGE/install.sh" \
&& rm -rf "$CILOCK_STAGE" \
&& "$PUSHGATE_PREFIX/bin/cilock" version \
| { read -r __cilock_version; [ "$__cilock_version" = 'cilock 3.1.3' ]; }
For release-policy and offline verification, use the CI/lock binary verification guide.
ALPS 1 · Authenticated
Mermaid source
flowchart LR
A[Coding agent] -->|invokes| C[CI/lock]
C -->|short-lived repository identity| F[Fulcio]
F -->|mandatory RFC 3161| T[TSA]
T --> E[Signed evidence]
X[No static signing key in agent config] -.-> CSummary
The platform authenticates a bounded workload identity and issues short-lived signing material for the exact evidence operation.
Intended for
Repositories that need independently verifiable signer identity and trusted signing time without a static evidence-signing key in agent configuration.
Requirements
After repository setup, the agent invokes the ordinary CI/lock command with no per-run platform, signer, or timestamp flags. When the agent invokes CI/lock, the identity is always an agent or workload principal — registered workflow OIDC in CI, or a stable registered agent identity — and never a human platform session inside the agent boundary. A human-attributed signature comes only from an explicit, server-observed interactive ceremony the human runs personally, outside the agent boundary; its output is handed off explicitly, never inherited by the agent.
This section previously published a runnable cilock run block for an agent. It signed with the credential held by the cilock session already on the host, which authenticates the account that session belongs to — so an agent launched from a signed-in shell inherited that principal and signed with no ceremony, and the evidence could not distinguish the agent's action from the account holder's. That is the principal separation the Pushgate agent-policy contract (docs/architecture/pushgate-agent-policy-contract.md in the Judge source tree) requires, so the recipe is withdrawn rather than reworded: publishing a runnable block is an instruction, and this one instructed readers into the wrong identity model. It returns when there is a stable agent/workload principal to bind it to — registered workflow OIDC, or a registered agent identity — or an explicit human ceremony performed outside the agent boundary and handed off. The requirement above still states what ALPS 1 asks for; what is missing is a command we are willing to tell you to paste, not the definition.
Evidence a verifier checks
ALPS 0 facts plus authenticated platform issuance, workload, tenant, repository, purpose, audience and time bindings, and the ephemeral signing event.
Benefits and mitigations
Mitigates long-lived static-key theft, cross-repository identity reuse, unsigned or timestamp-less substitution, replay, and stale-proof ambiguity.
Limits
The agent and CI/lock still share OS/user authority. Keyless issuance does not prove no other key, platform session, or socket existed in the agent environment, or that a permitted CI/lock operation was invoked honestly.
ALPS 2 · Constrained
Mermaid source
flowchart LR
subgraph S[Restricted agent sandbox]
A[Coding agent]
end
N[nono tool broker]
subgraph C[Fresh child-tool sandbox]
W[Pinned CI/lock + attestors]
end
F[Platform identity + Fulcio + TSA]
R[Signed in-toto / DSSE evidence]
A -->|cilock run| N -->|exact binary + narrow grant| W
W -->|keyless sign + timestamp| F --> RSummary
An enforced runtime boundary constrains the agent and a trusted external observer records the exact controls applied to the measured process.
Intended for
Repositories where prompt injection or compromised agent behavior must not directly reach signing authority, privileged sockets, host credentials, or unrestricted egress.
Requirements
ALPS 2 adds enforceable filesystem, environment, process, socket, and network boundaries. The agent sees only the repository, a synthetic home, approved model endpoints, and development tools. It receives no platform session, human key, SSH_AUTH_SOCK, signer cache, cloud credential, Keychain proxy, Docker socket, generic signer socket, or direct platform credential.
Each setup block below writes its configuration file with a here-document and then checks what landed on disk against the digest printed in the same block. That is a paste check: it catches a copy that was truncated, wrapped, or edited on the way to your terminal, which would otherwise install a half-written boundary silently. It is not provenance and not proof of authenticity. The digest is printed on the same page as the content it covers, so anyone able to change the content here could change the digest here too and the check would still pass. Authenticity for executable content comes from the signed CI/lock installer above, whose signature is checked against an identity this page cannot mint. Read the profile before you run it — that is why it is printed rather than downloaded.
Each configuration below proves credential and signer containment, not authenticated proof. None of them supplies the cumulative ALPS 1 authentication path: the boundary passes only generic environment variables and a synthetic home, and excludes every platform session, so cilock run inside these fixtures cannot produce the authenticated ALPS 1 proof — even where the nono child policy grants narrow platform egress. Authenticated signing requires an identity issued outside the agent boundary: registered workflow OIDC in CI, or an explicit human signing ceremony on the host. A deployable ALPS 2 composition additionally requires an independently mediated CI/lock path bound to such an identity.
A boxed agent that writes .git/hooks/pre-commit, or points core.hooksPath somewhere it controls via .git/config, has not escaped its sandbox — it has arranged for your next Git command to run its code outside the sandbox, with your signing keys and platform session. The boundary that matters is not where the agent runs; it is what runs after.
Sandbox Runtime closes this. Its profile denies writes to .git/hooks/** and .git/config while leaving them readable, so Git keeps working and the hook cannot be planted — measured against the runtime, not inferred from the schema.
nono 0.74.0 does not, and cannot. Measured, all four ways: deny blocks reads as well as writes and Git needs to read .git/config for every operation, so the boundary stops working at all; read and read_file are additive grants that do not restrict; and deny combined with a read re-grant leaves the read blocked. There is no write-only denial for a subpath of a read-write workdir. The signed receipt for this boundary records that honestly — hostGitMetadata: allowed, result: open — rather than reporting a pass. Use the Sandbox Runtime boundary where this matters, or run the agent under a separate UID or in a VM.
Every block below pins PATH to the base system and resolves each third-party tool to an absolute path, refusing any tool supplied from inside your workspace and any whose file or parent directory is world-writable. That defeats a repository-controlled PATH entry and a binary another local user could have replaced — a hostile repository. It does not prove the tool is genuine. Anyone already running as your own user can write $HOME/bin/nono or $HOME/bin/gh with ordinary 0755 permissions — outside the workspace, not world-writable — and it passes every check on this page: a fake gh attestation verify exits 0, a fake nono reports the expected version, and the agent is recorded as sandboxed while running with no sandbox at all. The same applies to cosign, curl and CI/lock, and to the attestation check that verifies nono. No file-permission test can close this, because the check runs with exactly the privileges of the process it would have to catch. The mitigation is a different privilege domain, not a stronger probe: run the agent under a separate UID, in a container, or in a VM, so that writing $HOME/bin is not something it can do.
One more gap worth knowing about, because it has a fix you can apply. The probe tests the other-write bit and not the group-write bit. Homebrew's default prefix is group-writable — measured on an Apple-silicon Mac, /opt/homebrew/bin is drwxrwxr-x <user>:admin, and Intel's /usr/local/bin has the same shape. So any member of that group can replace nono, gh, cosign or CI/lock before confinement starts, and these checks will still call the result host-owned. Unlike the same-UID case above, this one is a genuinely different principal from you. We do not reject it automatically: doing so refuses every Homebrew-installed tool, which is the install path this page itself recommends, so the choice is yours rather than ours. If other people have admin on the machine, close it — chmod g-w /opt/homebrew/bin, or install these tools somewhere that is not group-writable. The probe reports on the resolved binary and every directory above it, so a prefix you have tightened is checked all the way up.
nono 0.74.0
Store this profile outside the repository. command_policies resolves cilock to the exact non-workspace executable, denies direct-exec bypass, and creates a new child-tool sandbox for every approved run or verify invocation. Fake binaries, PATH shadows, and writable replacements receive no CI/lock command policy.
{
"$schema": "https://nono.sh/schemas/nono-profile.schema.json",
"extends": "default",
"meta": {
"name": "pushgate-agent-macos",
"version": "0.74.0",
"description": "Workspace-only coding-agent boundary with a pinned CI/lock child-tool policy"
},
"security": {
"signal_mode": "isolated",
"process_info_mode": "isolated",
"ipc_mode": "shared_memory_only",
"capability_elevation": false
},
"workdir": {
"access": "readwrite"
},
"filesystem": {
"deny": [
"$HOME/.ssh",
"$HOME/.gnupg",
"$HOME/.config/cilock",
"$HOME/.config/gitsign",
"$HOME/.config/sigstore",
"$HOME/.aws",
"$HOME/.azure",
"$HOME/.config/gcloud",
"$HOME/.kube",
"$HOME/.docker",
"$HOME/Library/Application Support/cilock",
"$HOME/Library/Application Support/gitsign",
"$HOME/Library/Application Support/sigstore",
"$HOME/Library/Caches/sigstore",
"$HOME/Library/Keychains",
"$HOME/Library/Containers/com.docker.docker",
"/var/run/docker.sock",
"/private/var/run/docker.sock",
"$HOME/.colima",
"$HOME/.orbstack",
"$HOME/.rd"
]
},
"network": {
"allow_domain": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
]
},
"environment": {
"allow_vars": [
"PATH",
"TERM",
"COLORTERM",
"LANG",
"LC_ALL",
"TMPDIR"
],
"deny_vars": [
"SSH_AUTH_SOCK",
"GPG_AGENT_INFO",
"DOCKER_HOST",
"CILOCK_*",
"GITSIGN_*",
"SIGSTORE_*",
"AWS_*",
"AZURE_*",
"GOOGLE_*",
"GCP_*",
"KUBECONFIG",
"GH_TOKEN",
"GITHUB_TOKEN"
],
"set_vars": {
"HOME": "$WORKDIR/.pushgate/agent-home",
"XDG_CONFIG_HOME": "$WORKDIR/.pushgate/agent-home/.config",
"XDG_CACHE_HOME": "$WORKDIR/.pushgate/agent-home/.cache",
"CODEX_HOME": "$WORKDIR/.pushgate/agent-home/.codex",
"CLAUDE_CONFIG_DIR": "$WORKDIR/.pushgate/agent-home/.claude",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"DISABLE_TELEMETRY": "1",
"DISABLE_ERROR_REPORTING": "1"
}
},
"command_policies": {
"deny_direct_exec_bypass": [
"/opt/homebrew/bin/cilock"
],
"commands": {
"cilock": {
"executable": "/opt/homebrew/bin/cilock",
"from": {
"session": {
"sandbox": {
"fs_read": [
"."
],
"fs_write": [
"."
],
"network": {
"allow_domain": [
"platform.testifysec.com"
]
},
"environment": {
"allow_vars": [
"HOME",
"PATH",
"TERM",
"TMPDIR"
]
}
},
"invocation_policy": {
"default": "deny",
"allow": [
{
"argv": {
"prefix": [
"run"
]
}
},
{
"argv": {
"prefix": [
"verify"
]
}
},
{
"argv": {
"exact": [
"version"
]
}
}
]
}
}
}
}
}
}
}
Set up and launch
The block below writes that profile itself, from the same bytes shown above, so nothing here depends on a file you do not have. The SHA-256 check that follows it confirms the paste arrived intact; it is not a provenance or authenticity claim.
# ALPS 0.1 supported environment for this boundary: Apple-silicon macOS.
# The nono profile below pins /opt/homebrew paths and the digest printed
# beside it covers those exact bytes, so the profile cannot be re-pointed per
# host without giving up the paste check. On any other Homebrew prefix this
# block REFUSES rather than installing a boundary that cannot work. Intel macOS
# support is pending.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the cosign verification and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the cosign verification and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
if pushgate_host_has brew; then
PUSHGATE_BREW="$(pushgate_host_tool brew)" || exit 1
PUSHGATE_PREFIX="$("$PUSHGATE_BREW" --prefix)"
else PUSHGATE_PREFIX='/opt/homebrew'; fi
case "$PUSHGATE_PREFIX" in
"$PWD"/*) echo 'refusing: the installation prefix resolves inside the workspace' >&2; exit 1 ;;
/*) ;;
*) echo 'refusing: the installation prefix is not an absolute path' >&2; exit 1 ;;
esac
[ "$PUSHGATE_PREFIX" = '/opt/homebrew' ] || {
echo "refusing: this profile pins /opt/homebrew/bin/cilock and its digest, and this host's Homebrew prefix is $PUSHGATE_PREFIX — the pinned nono profile is Apple-silicon only" >&2; exit 1; }
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
PUSHGATE_NONO="$(pushgate_host_tool nono)" || exit 1
PUSHGATE_GH="$(pushgate_host_tool gh)" || exit 1
"$PUSHGATE_GH" attestation verify "$PUSHGATE_NONO" --repo nolabs-ai/nono >/dev/null || {
echo 'refusing: this nono carries no verifiable build provenance from nolabs-ai/nono' >&2; exit 1; }
test "$("$PUSHGATE_NONO" --version)" = "nono 0.74.0" || {
echo 'refusing: nono 0.74.0 is required' >&2; exit 1; }
mkdir -p "$HOME/.config/pushgate" || exit 1
printf '%s\n' "$PUSHGATE_NONO" > "$HOME/.config/pushgate/nono.path" || exit 1
$PUSHGATE_SHA256 "$PUSHGATE_NONO" > "$HOME/.config/pushgate/nono.sha256" || exit 1
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$HOME/.config/pushgate/nono-macos.json" <<'PUSHGATE_NONO_PROFILE'
{
"$schema": "https://nono.sh/schemas/nono-profile.schema.json",
"extends": "default",
"meta": {
"name": "pushgate-agent-macos",
"version": "0.74.0",
"description": "Workspace-only coding-agent boundary with a pinned CI/lock child-tool policy"
},
"security": {
"signal_mode": "isolated",
"process_info_mode": "isolated",
"ipc_mode": "shared_memory_only",
"capability_elevation": false
},
"workdir": {
"access": "readwrite"
},
"filesystem": {
"deny": [
"$HOME/.ssh",
"$HOME/.gnupg",
"$HOME/.config/cilock",
"$HOME/.config/gitsign",
"$HOME/.config/sigstore",
"$HOME/.aws",
"$HOME/.azure",
"$HOME/.config/gcloud",
"$HOME/.kube",
"$HOME/.docker",
"$HOME/Library/Application Support/cilock",
"$HOME/Library/Application Support/gitsign",
"$HOME/Library/Application Support/sigstore",
"$HOME/Library/Caches/sigstore",
"$HOME/Library/Keychains",
"$HOME/Library/Containers/com.docker.docker",
"/var/run/docker.sock",
"/private/var/run/docker.sock",
"$HOME/.colima",
"$HOME/.orbstack",
"$HOME/.rd"
]
},
"network": {
"allow_domain": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
]
},
"environment": {
"allow_vars": [
"PATH",
"TERM",
"COLORTERM",
"LANG",
"LC_ALL",
"TMPDIR"
],
"deny_vars": [
"SSH_AUTH_SOCK",
"GPG_AGENT_INFO",
"DOCKER_HOST",
"CILOCK_*",
"GITSIGN_*",
"SIGSTORE_*",
"AWS_*",
"AZURE_*",
"GOOGLE_*",
"GCP_*",
"KUBECONFIG",
"GH_TOKEN",
"GITHUB_TOKEN"
],
"set_vars": {
"HOME": "$WORKDIR/.pushgate/agent-home",
"XDG_CONFIG_HOME": "$WORKDIR/.pushgate/agent-home/.config",
"XDG_CACHE_HOME": "$WORKDIR/.pushgate/agent-home/.cache",
"CODEX_HOME": "$WORKDIR/.pushgate/agent-home/.codex",
"CLAUDE_CONFIG_DIR": "$WORKDIR/.pushgate/agent-home/.claude",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"DISABLE_TELEMETRY": "1",
"DISABLE_ERROR_REPORTING": "1"
}
},
"command_policies": {
"deny_direct_exec_bypass": [
"/opt/homebrew/bin/cilock"
],
"commands": {
"cilock": {
"executable": "/opt/homebrew/bin/cilock",
"from": {
"session": {
"sandbox": {
"fs_read": [
"."
],
"fs_write": [
"."
],
"network": {
"allow_domain": [
"platform.testifysec.com"
]
},
"environment": {
"allow_vars": [
"HOME",
"PATH",
"TERM",
"TMPDIR"
]
}
},
"invocation_policy": {
"default": "deny",
"allow": [
{
"argv": {
"prefix": [
"run"
]
}
},
{
"argv": {
"prefix": [
"verify"
]
}
},
{
"argv": {
"exact": [
"version"
]
}
}
]
}
}
}
}
}
}
}
PUSHGATE_NONO_PROFILE
printf '%s %s\n' 'af1c5d4c64f6e1b83adf5ca0b38761c8575bdc4bf4adf95bd8c27e4475de166e' "$HOME/.config/pushgate/nono-macos.json" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$HOME/.config/pushgate/nono-macos.json"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored — add ".pushgate/" to .gitignore' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
PUSHGATE_NONO= PUSHGATE_NONO_DIGEST=
read -r PUSHGATE_NONO 2>/dev/null < "$HOME/.config/pushgate/nono.path" || true
[ -n "$PUSHGATE_NONO" ] || {
echo 'refusing: no attested nono on record — run the setup block above first' >&2; exit 1; }
read -r PUSHGATE_NONO_DIGEST PUSHGATE_NONO_DIGEST_SUBJECT 2>/dev/null < "$HOME/.config/pushgate/nono.sha256" || true
[ -n "$PUSHGATE_NONO_DIGEST" ] || {
echo 'refusing: no recorded digest for the attested nono — run the setup block above first' >&2; exit 1; }
printf '%s %s\n' "$PUSHGATE_NONO_DIGEST" "$PUSHGATE_NONO" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the attested nono has changed since it was verified' >&2; exit 1; }
PATH="$PUSHGATE_AMBIENT_PATH" "$PUSHGATE_NONO" run --profile "$HOME/.config/pushgate/nono-macos.json" \
--workdir "$PWD" --allow-cwd -- codex --sandbox workspace-write --ask-for-approval on-request
macOS with Anthropic Sandbox Runtime 0.0.73
Sandbox Runtime 0.0.73
This containment-only fixture keeps runtime settings outside the workspace. A broad read deny for user and temporary volumes is followed by narrow workspace and toolchain carve-outs. Unix sockets, local listeners, Apple Events, and the weaker nested modes remain disabled. Because it provides neither platform signing egress nor a separate CI/lock mediator, it does not by itself satisfy cumulative ALPS 1 or ALPS 2.
{
"network": {
"allowedDomains": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
],
"deniedDomains": [],
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false
},
"filesystem": {
"denyRead": [
"/Users",
"/private",
"/Volumes"
],
"allowRead": [
".",
"./**",
"/bin",
"/dev",
"/Library",
"/opt/homebrew",
"/private/var/select",
"/System",
"/usr",
"/usr/local"
],
"allowWrite": [
".",
"./**"
],
"denyWrite": [
"./.git/hooks/**",
"./.git/config"
]
},
"enableWeakerNestedSandbox": false,
"enableWeakerNetworkIsolation": false,
"allowAppleEvents": false
}
Set up and launch
As above, the block writes the settings file from the bytes shown here rather than from any path in a repository. The SHA-256 check that follows it confirms the paste arrived intact; it is not a provenance or authenticity claim.
# ALPS 0.1 supported environment for this boundary: macOS on Apple silicon and
# on Intel. Linux is covered by the container boundary, not by this block.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the cosign verification and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the cosign verification and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
PUSHGATE_NPM="$(pushgate_host_tool npm)" || exit 1
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" "$PUSHGATE_NPM" install --prefix "$HOME/.local/share/pushgate/srt-0.0.73" \
--save-exact @anthropic-ai/sandbox-runtime@0.0.73 || {
echo 'refusing: the pinned Sandbox Runtime install failed' >&2; exit 1; }
test -x "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" || {
echo 'refusing: no Sandbox Runtime at $HOME/.local/share/pushgate/srt-0.0.73' >&2; exit 1; }
mkdir -p "$HOME/.config/pushgate" || exit 1
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$HOME/.config/pushgate/srt-macos.json" <<'PUSHGATE_SRT_SETTINGS'
{
"network": {
"allowedDomains": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
],
"deniedDomains": [],
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false
},
"filesystem": {
"denyRead": [
"/Users",
"/private",
"/Volumes"
],
"allowRead": [
".",
"./**",
"/bin",
"/dev",
"/Library",
"/opt/homebrew",
"/private/var/select",
"/System",
"/usr",
"/usr/local"
],
"allowWrite": [
".",
"./**"
],
"denyWrite": [
"./.git/hooks/**",
"./.git/config"
]
},
"enableWeakerNestedSandbox": false,
"enableWeakerNetworkIsolation": false,
"allowAppleEvents": false
}
PUSHGATE_SRT_SETTINGS
printf '%s %s\n' '1f7db33fc89d14e44a6d2f6c16e184e5aaa661ef9efac3ab6abb249a2f2423d2' "$HOME/.config/pushgate/srt-macos.json" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$HOME/.config/pushgate/srt-macos.json"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored — add ".pushgate/" to .gitignore' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
env -i HOME="$PWD/.pushgate/agent-home" \
XDG_CONFIG_HOME="$PWD/.pushgate/agent-home/.config" \
CODEX_HOME="$PWD/.pushgate/agent-home/.codex" \
CLAUDE_CONFIG_DIR="$PWD/.pushgate/agent-home/.claude" \
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" \
TERM="${TERM:-xterm-256color}" \
"$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" \
--settings "$HOME/.config/pushgate/srt-macos.json" -- codex --sandbox workspace-write --ask-for-approval on-request
Linux in a Colima or Docker container
Container boundary
This containment-only fixture runs without the host network, host home, Docker socket, SSH agent socket, or added capabilities. Mount only the repository. Its disabled network also blocks the cumulative ALPS 1 signing path, so it does not by itself satisfy ALPS 2. Coding agents cannot run inside this fixture: the image installs no agent CLI and the disabled network blocks model-endpoint egress. The command below writes the controls Dockerfile into a private temporary directory of its own, builds the fixture there from a digest-pinned Debian base, and opens an interactive shell inside the boundary for inspecting its controls. It needs no file from this repository and writes nothing into yours. A deployable Linux agent composition needs an agent-bearing digest-pinned image, an approved egress allowlist, and an independently mediated CI/lock path with narrow platform, Fulcio, and TSA access.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the cosign verification and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the cosign verification and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
PUSHGATE_DOCKER="$(pushgate_host_tool docker)" || exit 1
CONTROLS_IMAGE_ID= CONTROLS_CONTEXT_DIR="$(pushgate_host_stage)" || {
echo 'refusing: could not create a private build context' >&2; exit 1; }
[ -n "$CONTROLS_CONTEXT_DIR" ] || {
echo 'refusing: empty build context path' >&2; exit 1; }
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$CONTROLS_CONTEXT_DIR/Dockerfile.controls" <<'PUSHGATE_CONTROLS_DOCKERFILE'
# The runtime flags under test are documented in /docs/agent-sandbox. This
# fixture adds only Git and OpenSSH so the negative signed-commit control can
# run. Base image digest observed from Docker's registry on 2026-08-25.
FROM debian@sha256:b1a741487078b369e78119849663d7f1a5341ef2768798f7b7406c4240f86aef
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git netcat-openbsd openssh-client \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
PUSHGATE_CONTROLS_DOCKERFILE
printf '%s %s\n' '9318bcae6a23789ac8dec26afaf9ad6587c6e09a6d5f6cf127662eea3d03d341' "$CONTROLS_CONTEXT_DIR/Dockerfile.controls" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$CONTROLS_CONTEXT_DIR/Dockerfile.controls"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
CONTROLS_BUILD_TAG="pushgate-agent-sandbox-controls:${CONTROLS_CONTEXT_DIR##*/}" \
&& PATH="$PUSHGATE_AMBIENT_PATH" "$PUSHGATE_DOCKER" build --provenance=false --sbom=false -t "$CONTROLS_BUILD_TAG" \
-f "$CONTROLS_CONTEXT_DIR/Dockerfile.controls" \
"$CONTROLS_CONTEXT_DIR" \
&& CONTROLS_IMAGE_ID="$("$PUSHGATE_DOCKER" image inspect "$CONTROLS_BUILD_TAG" \
--format '{{.Id}}')" \
&& "$PUSHGATE_DOCKER" run --rm -it --read-only --network none \
--cap-drop ALL --security-opt no-new-privileges --pids-limit 512 \
--user "$(id -u):$(id -g)" --env HOME=/tmp/agent-home \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
--mount type=bind,src="$PWD",dst=/workspace --workdir /workspace \
"$CONTROLS_IMAGE_ID" /bin/sh
The container receives a fresh in-memory home. Do not add -v $HOME, -v /var/run/docker.sock, --privileged, --network host, or an SSH_AUTH_SOCK mount.
The launch reference is the image's config digest, held in the $CONTROLS_IMAGE_ID shell variable. The build tag is only a handle for the docker image inspect that resolves it; nothing is launched by a tag. The digest is deliberately never written to a file inside the repository: a predictable workspace path is one the agent you are boxing in can write, and it could pre-create that path as a symlink to redirect the write, or swap the value before docker run reads it back. A variable has neither weakness, and this recipe writes nothing into the workspace at all. The build context is a fresh mktemp -d for the same reason: a context inside your repository is a context the agent you are boxing in can edit, and the Dockerfile it edits is the image that then gets launched over that repository. Every step is chained with &&, so a failed build never reaches the inspect and a failed inspect never reaches docker run. --provenance=false --sbom=false is what makes the digest reproducible — without them BuildKit wraps each build in a per-invocation OCI image index and the digest changes every time, even when the layers and image config are byte-identical. Two builds of these same inputs on the same platform therefore produce the same digest; a build on a different architecture is a different image by construction. The apt packages are unversioned, so a build made after Debian publishes new packages also legitimately differs — a mismatch means different bytes, not a broken check. Every isolation flag above is rendered from a single definition in the page source rather than retyped, so the command shown here and the command any check of it reproduces are the same string.
Launch the coding agent
Choose your coding agent. Every launch below starts it inside one of the documented macOS boundaries, which keep signing authority outside the agent process. The Linux container fixture hosts no coding agent. Run the matching setup from the boundary section above first.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the cosign verification and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the cosign verification and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored — add ".pushgate/" to .gitignore' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
PUSHGATE_NONO= PUSHGATE_NONO_DIGEST=
read -r PUSHGATE_NONO 2>/dev/null < "$HOME/.config/pushgate/nono.path" || true
[ -n "$PUSHGATE_NONO" ] || {
echo 'refusing: no attested nono on record — run the setup block above first' >&2; exit 1; }
read -r PUSHGATE_NONO_DIGEST PUSHGATE_NONO_DIGEST_SUBJECT 2>/dev/null < "$HOME/.config/pushgate/nono.sha256" || true
[ -n "$PUSHGATE_NONO_DIGEST" ] || {
echo 'refusing: no recorded digest for the attested nono — run the setup block above first' >&2; exit 1; }
printf '%s %s\n' "$PUSHGATE_NONO_DIGEST" "$PUSHGATE_NONO" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the attested nono has changed since it was verified' >&2; exit 1; }
PATH="$PUSHGATE_AMBIENT_PATH" "$PUSHGATE_NONO" run --profile "$HOME/.config/pushgate/nono-macos.json" \
--workdir "$PWD" --allow-cwd -- codex --sandbox workspace-write --ask-for-approval on-request
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the cosign verification and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the cosign verification and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored — add ".pushgate/" to .gitignore' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
env -i HOME="$PWD/.pushgate/agent-home" \
XDG_CONFIG_HOME="$PWD/.pushgate/agent-home/.config" \
CODEX_HOME="$PWD/.pushgate/agent-home/.codex" \
CLAUDE_CONFIG_DIR="$PWD/.pushgate/agent-home/.claude" \
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" \
TERM="${TERM:-xterm-256color}" \
"$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" \
--settings "$HOME/.config/pushgate/srt-macos.json" -- codex --sandbox workspace-write --ask-for-approval on-request
Model-client authentication stays inside the synthetic home and receives no TestifySec, Git-signing, or infrastructure authority.
Evidence a verifier checks
ALPS 1 facts plus measured agent and CI/lock executable digests, the sandbox-policy digest actually loaded, process ancestry, exec-time environment allowlist, mount and rootfs state, workspace/toolchain/cache digests, filesystem/network policy, and observer-covered egress and denials. Negative proof is limited to the observer’s stated coverage.
Benefits and mitigations
Mitigates prompt-injection attempts to read or exfiltrate signing material, generic signer-socket access, PATH or binary substitution, direct credential use, and unapproved egress.
Limits
The sandbox, kernel, runtime, allowlist, exact CI/lock binary, attestors, and test program remain trusted. A sandbox escape, host administrator, compromised allowed tool, dishonest test, or policy mistake is out of scope. Evidence proves observed execution and output—not semantic correctness.
Derive hermeticity separately
Mermaid source
flowchart LR
O[Trusted observer evidence] --> D{Derive separately from ALPS 0.1}
D --> A[H-Open: external influence permitted and recorded]
D --> C[H-Constrained: allowlist enforced and inputs observed]
D --> H[H-Complete: no unmeasured influence crosses named boundary]
M[Missing or insufficient coverage] --> U[Hermeticity: Unknown]ALPS 0.1 measures provenance and isolation between an agent and signing authority. Hermeticity measures external runtime influence. A verifier derives the two results independently and can report a compact pair such as ALPS-2 / H-Constrained.
Evidence a verifier checks
- H-Open: signed observations identify the external runtime influence that was permitted and record relevant destinations and inputs.
- H-Constrained: a trusted boundary enforced an explicit allowlist—including named public or private API providers—and observed allowed destinations and relevant external inputs.
- H-Complete: all material inputs were staged and content-identified; network was disabled or confined entirely within the independently measured boundary; and a trusted observer verified that no unmeasured external influence crossed it.
Limits
The evidence must name the assessed boundary and its scope. Ambient environment, file descriptors, sockets, filesystem roots, toolchain, caches, time, randomness, and host inputs must be bounded or explicit within that coverage. An on-premises customer-operated inference cluster or independently attested dedicated cloud inference environment can be inside an expanded closed boundary. A managed model endpoint reached through standard private connectivity such as AWS PrivateLink remains H-Constrained when the provider’s model or runtime influence is not fully measured. “No observed network” alone is not proof of hermeticity. Missing or insufficient evidence produces Hermeticity: Unknown, not H-Open.
ALPS 3 · Isolated
Pushgate packages ALPS 3 administration and protected signing services in this product tier. The ALPS level name remains vendor-neutral.
Mermaid source
flowchart LR
A[Agent sandbox] -->|typed proof request| D[CI/lock service]
D -->|validate repo + commit + command + output| H[Non-exportable hardware identity]
H -->|keyless leaf + RFC 3161| P[TestifySec platform]
P --> E[Signed evidence]
G[No generic signing oracle] -.-> DSummary
A separate service independently re-derives each typed proof request before protected signing authority can authorize it.
Intended for
Organizations that treat the coding-agent process as potentially fully compromised or adversarial.
Requirements
The same CI/lock distribution has two roles: an unprivileged client in the agent sandbox and a separate service or control plane outside it. The service accepts only typed proof requests and independently re-derives repository, commit, command, predicate, output digest, expiry, and replay context. It never exposes a sign-arbitrary-bytes endpoint.
The service bootstrap identity is non-exportable: TPM 2.0, an HSM, Apple Secure Enclave on Apple silicon, the T2 Security Chip on older Intel Macs, or a custom KMS solution. macOS deployments verify Developer ID, notarization, and a designated requirement. Linux deployments verify signed packages or container digests and can add fs-verity/IMA plus TPM measured-boot quotes. Hardware usually protects the daemon, device, or workload bootstrap identity; Fulcio can still mint an ephemeral leaf for each evidence signature, and every signature remains RFC 3161 timestamped.
Evidence a verifier checks
ALPS 2 facts plus typed-request re-derivation, measured and signed CI/lock, isolated service identity, hardware or remote-key attestation, and evidence that signing authority was unavailable to the agent process.
Benefits and mitigations
Mitigates full agent-process compromise, agent-side key extraction, arbitrary-bytes signing, local binary substitution, and direct access to signing authority.
Limits
The service and platform implementations, hardware root and administrator controls, policy, verifier roots, TSA, and the tools whose outputs are attested remain trusted. This level does not prove code is bug-free or tests are sufficient.
Verify the boundary before onboarding
Run these controls with disposable fixtures. Every denied signing attempt must leave HEAD and repository refs unchanged.
- Read a synthetic key outside the workspace directly and through a workspace symlink: both must fail at ALPS 2.
- Try inherited and guessed SSH-agent, Docker, and signer sockets: they must be absent, unmounted, and unreachable.
- Try a fake
cilock, PATH shadow, direct executable path, and writable replacement: none may inherit the pinned command policy. - Run CI/lock twice: both approved invocations work, and each receives a fresh child-tool policy.
- Attempt direct platform access from the agent: the egress and credential policy must deny it.
- Produce proof through the approved path and verify its observer facts, exact subjects, Fulcio identity, and RFC 3161 timestamp.
Scope notes
Pin Sandbox Runtime 0.0.73 and retest it before upgrades. Linux pathname sockets must be absent from the child namespace, not merely removed from its environment. These containment fixtures do not treat macOS Keychain, Secure Enclave, or T2 custody as evidence of signer isolation; ALPS 3 requires independently verified non-exportability evidence.