hamsa software
← All articles
Project notes7 min read

The agent can fetch the code without holding the key

You need the repository; the host keeps the reusable key. WardOS checks what the agent asks to do—and whether a passing test still describes the files being reviewed.

A heavy arch encloses a passage with a circle at the end of an incoming line and a separate outgoing line, suggesting access mediated through a bounded host route.

Access to the code, without custody of the key

You ask a coding agent to fetch a repository. It needs access to the code, so you give it a GitHub token. Job done. Except you’ve now given it something reusable that may open considerably more than the repository you had in mind. “Please fetch this project” has quietly become “Please look after this key.”

WardOS takes a different approach. For its supported GitHub gateway, the host keeps the token and checks requests before using it. Fetch the approved repository? That can proceed. Push changes through a read-only grant? Refused. Try another repository? Refused again. The agent can even edit its Git remote, but the grant still points to the repository fixed when the session began. Changing the address on the envelope doesn’t change where the host is willing to deliver it.[2][3][4]

That’s the useful distinction: the agent gets a way to do an authorised job without receiving the reusable credential behind it. Its instructions can say “read only”. The gateway gives those words something solid to stand on.

WardOS is Hamsa Software’s Linux workstation and execution-runtime project for coding agents.[1] This note follows public commit 1732a0cc: the GitHub gateway and the checks behind its verification indicator. The project’s tests were inspected, rather than independently run for this article.

The agent asks for the repository

Take a session working on hexrift/WardOS. The agent needs to fetch code before it can change anything. The host first checks the policy: deny refuses access, ask requires an explicit GitHub grant, and allow can proceed automatically. It also needs a token available. If the adapter cannot identify the approved repository, no grant is created.[2] An unresolved destination is a reason to stop, rather than an invitation to let the agent choose one.

The host fixes that repository when the session starts. Later, the agent edits its Git remote to point somewhere else. It can change the project configuration, but its grant still names the original repository.[3] This is a necessary separation for a coding tool: the files it is allowed to edit cannot also become the place where it rewrites the host’s permission decision.

The fetch travels through a local relay. The host supplies authentication when forwarding an approved request to GitHub, so the agent can obtain the code without receiving the token. Supported API clients have a separate route to ask through. An unrelated HTTPS connection does not gain that credential simply because it came from the same sandbox. The gateway settings below explain which client routes participate; they are supporting configuration, not a promise that every GitHub tool will work unchanged.

Reviewed gateway reference
Approved repository: SessionMeta::origin_repo
Linked worktree metadata: gitdir and commondir
Permitted Git paths: /hexrift/WardOS and /hexrift/WardOS.git
Permitted REST path: /repos/hexrift/WardOS
Local relay: 127.0.0.1:3128
Git: /github → github.com over TLS; Basic x-access-token
REST: /github-api → api.github.com over TLS; Bearer authentication
Compatible API client setting: GITHUB_API_URL

Fetching works. Pushing needs a different permission.

A fetch is not a single request. Git first asks about available references, then uses its upload-pack service to receive objects. That second step uses POST, even though it belongs to the read-only operation.[3][4] Blocking every POST would therefore prevent the ordinary fetch we wanted to allow. The two request examples show why the host needs a more specific rule than a list of supposedly safe methods.

For the approved repository, GET, HEAD and OPTIONS pass. A POST passes only when the path, with its query removed, ends in /git-upload-pack. A push uses /git-receive-pack and is refused, along with PUT, PATCH and DELETE.[4] The proxy returns 403 and records that refusal before resolving or connecting upstream. The agent’s request reaches a decision at the host before it reaches GitHub.

Changing the repository name fails separately from changing the operation. The grant accepts the repository’s path segments, not any name beginning with the same letters. Adding an extra character or placing a permitted operation in the query string cannot turn another repository or operation into the approved one. The proxy removes the local route prefix before these checks; the matrix shows requests sent through the relay. The project’s integration tests exercise allowed fetches and refused requests against a controlled upstream.[9]

I would check these rules against the tool’s actual requests before promising it GitHub access. The gateway does not classify every REST endpoint as issues:read or contents:read; GitHub also applies the token’s permissions. A legitimate read exposed through another POST can be refused. That is a useful constraint to discover during integration, when a failed read is still a tool-compatibility problem rather than a surprise in the middle of the job.

HTTP request examples
GET /github/hexrift/WardOS.git/info/refs?service=git-upload-pack
POST /github/hexrift/WardOS.git/git-upload-pack
The read-only GitHub route decides before injection

Request examples derived from the reviewed public adapter and proxy rules for hexrift/WardOS. The local relay strips its route prefix before scope checking.

Scroll sideways to see all columns.

Request through local relayRoute decisionReason
GET /github/hexrift/WardOS.git/info/refs?service=git-upload-packPass route checkGranted repository; GET
POST /github/hexrift/WardOS.git/git-upload-packPass route checkFetch POST exception
POST /github/hexrift/WardOS.git/git-receive-pack403 scope refusalWrite not granted
GET /github/hexrift/WardOS.gitx/info/refs403 scope refusalDifferent path segment
GET /github-api/repos/hexrift/WardOS/issuesPass route checkGranted REST repository; GET
Source-derived decisions, not a newly executed integration trial. An allowed route request still depends on GitHub’s own authorisation. The rule is repository path plus method/write scope.

Stopping the route does not always retire the key

Now end the session’s GitHub grant. Further requests through that route stop, but its host-held token may remain valid. The built-in route obtains that token from a host variable or vault file.[2] Keeping it away from the agent has reduced who handles the secret. If the host-side copy is exposed, its owner still needs to replace or revoke it at GitHub.

A credential provider can offer a different arrangement. With Vault or OpenBao token-role leases, the provider sets the token’s lifetime and WardOS can request revocation using a reference called an accessor. A KV secret is a stored value: closing its WardOS route leaves the underlying secret valid, as with the local vault.[2] Choose between those arrangements according to whether the job needs an end to the route or an end to the credential itself.

The proxy checks for revoked grants and expired leases before scope checking and again at the relevant sending boundary.[11] Source revocation may remain unconfirmed if the provider is unavailable. A request already handed to a socket cannot be recalled by withdrawing the grant or pausing the agent. The receiving service’s records are still needed to discover what that earlier request did.

The agent edits again. Does the green mark still belong?

Fetching the repository is only the start. The agent changes the code and asks for verification. ward verify captures a candidate snapshot, creates a fresh scratch tree and restores protected verification inputs from the entry snapshot. It runs the verifier offline in isolation.[5] Editing the worktree’s test cannot simply replace the protected starting test and award the candidate a pass.

The writer must stop while that snapshot is taken. Otherwise the capture can collect files from different moments, producing a combination the agent never actually had on disk. WardOS takes a host-owned hold, confirms that changes have stopped and keeps the hold through the snapshot walk. Its integration test compares inconsistent captures without that hold with consistent held captures.[6] Freezing the files still leaves the quality of the protected tests to their author.

After a pass, the agent can edit again. The reading shell compares a digest—a fingerprint of the current worktree—with the candidate named by VerificationPassed. A different digest makes the pass stale. If reading fails or the observation is interrupted, freshness becomes unknown and the current green assurance is removed.[7][10] The old result remains in the history; the person reviewing changed files needs to see why it no longer describes them.

That comparison can itself finish late. An edit may invalidate the workspace while its digest is being calculated. WardOS discards the older result when a newer invalidation has occurred.[10] Otherwise yesterday’s answer could arrive last and restore the green mark beside changed work. The result needs to belong to the version being reviewed, rather than win a race between calculations.

A recorded pass needs a current workspace comparison

The reviewed WardOS freshness path is separate from its GitHub credential gateway.

Scroll sideways to see all columns.

ObservationCurrent assurance
Worktree digest matches verified candidatePass refers to the observed candidate state
Digest differs after an editStale; historical pass remains recorded
Read interrupted or unreadableUnknown freshness; remove current green assurance
Late digest after newer invalidationDiscard the outdated observation
These are reviewed source behaviours and project-reported evidence, not independently executed tests in this article.

Before putting it on a workstation

The runtime needs Linux facilities including bubblewrap and unprivileged user namespaces. Its daemon, proxy and snapshot store remain trusted; host root compromise or kernel privilege escalation sits outside this protection.[1][6] An approved destination can also receive information. Limiting where a request goes does not decide whether its contents were sensible to disclose.

The selftest uses CANNOT-MEASURE-HERE when the host cannot check a property. The documented bubblewrap path, for example, reports a missing cgroup resource limit instead of counting it as passed.[6] Before giving an agent a real task, check what that machine actually obtained. A design feature and a successful local check are different reasons to rely on a boundary.

The reviewed documentation also leaves cloud STS backends and newly minted-token delivery for future work.[2] At-rest encryption, update rollback and a distinct verifier user identity remain security-model targets.[6][8] The README warns that main may be newer than a released binary.[1] Installation therefore needs a version decision as well as a list of promising features.

For the session fetching this repository, I would keep two answers visible: which requests the host permits, and which files the verifier passed. The agent can carry on editing. Its ability to do that work should come from a bounded route, while the person reviewing the next change can tell when the earlier pass has stopped applying.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. WardOS README at the reviewed commit
  2. Credential broker: GitHub scope, provider lifetimes and revocation
  3. GitHub adapter: pinned origin and generated routes
  4. Gateway route path and method checks
  5. Candidate preparation and protected entry inputs
  6. Security model: host assumptions, evidence and remaining targets
  7. Verification freshness state machine
  8. Project status at the reviewed commit
  9. Project proxy integration tests for read-only GitHub scope
  10. Freshness invalidation and late-digest generation guard
  11. Proxy revocation, expiry and pre-injection checks
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
AI developments7 min read

Why replacing a compromised key may not end an incident

A saved replacement, a running service and an attacker’s existing access can change at different times. Railway’s guidance explains why that matters; PingCAP and PHP show where the next job differs.

Integrations7 min read

A valid signature still needs a trust decision

An Oprindo sample had a matching asset hash and a valid signature, alongside an untrusted credential result. The useful story is in those separate answers.