Repository navigation
A coding instance can push to and open PRs on any repo the user can reach, not just its registered one #676
Description
A coding instance's registered repo is a working-directory default, not an authorization boundary. Nothing prevents an instance from writing to a completely different repository.
Observed today, unambiguously:
- Instance
e4d2d031-833b-4ccf-8b39-8af08337075b("PAS Coder") has exactly one repo registered:proappstore-online/platform, workdir~/dev/stores/pas/platform.coding_repos_listreturns that single entry and nothing else. - It was given an objective to implement
proappstore-online/platformissue Refactor: split agent-storage.ts (1262 LOC) per storage domain #137 (an MCP tool to edit an existing board ticket). - The run reported success — and had pushed branch
feat/update-board-tickettoProAgentStore/platformand opened PR feat(mcp): a board ticket can be edited, not only filed and moved #675 there. A different GitHub organisation entirely. proappstore-online/platformreceived nothing: no branch, no PR, no commits. The work exists only in the other org's repo.- The run's own closing summary described the target as "the PAGS platform repo", so the engine's model of where it was working had drifted, and no layer beneath it disagreed.
Root cause: the runner's git and gh credentials are the user's own account credentials, which reach every repo and org that account can write to. The registered repo scopes where the agent starts, not what it can reach. A run whose reasoning drifts to the wrong repo will succeed at writing there.
This is a containment problem rather than a prompting problem. Instructing objectives more carefully reduces the odds; it does not make the wrong-repo write impossible.
Requested change — scope credentials per instance via the GitHub App:
- Issue each coding instance a GitHub App installation token scoped to its registered repo, and use that token for the runner's git and
ghoperations instead of the account-wide credential.githubAppalready shows as configured incoding_diagnostics, so the mechanism is present. - Write access should be limited to the instance's registered repo. A push, branch create, PR open, merge or issue write against any other repo should fail at the credential layer, not rely on the engine choosing correctly.
- Read access should remain broader — across the user's own repos. This matters and should not be lost: in a separate run today, the agent usefully consulted
proappstore-online/platformPR Refactor: extract sub-hooks from packages/sdk/src/voice/use-voice.ts (1127 LOC) #138 as a reference implementation while working on ProAgentStore/platform. Scoping reads down to a single repo would break that legitimate pattern. - When a write is refused for being out of scope, surface the refused target repo in the run's stop reason, so the user sees "attempted write to X, not permitted" rather than a generic failure.
- Consider surfacing the effective token scope in
coding_diagnostics, so a user can confirm what an instance is actually able to write to before giving it autonomous work.
Reported by the account owner after a run landed a completed feature in the wrong organisation's repository and reported success.
Activity
proagentstore commentedon Aug 16, 2026
Follow-up finding that sharpens this issue: the boundary already exists at one layer and is absent at another.
Attempting cleanup of the mis-targeted PR from this same instance (
e4d2d031-833b-4ccf-8b39-8af08337075b, registered solely toproappstore-online/platform):github_create_commentagainstProAgentStore/platform→ 403. The MCP GitHub tools are correctly scoped to the instance's registered repo, and refused.ProAgentStore/platform— successfully.So the MCP tool layer enforces repo scope. The runner's shell layer —
git push,gh pr create— does not, because it uses the account-wide credential. An agent that stays within MCP tools is contained; an agent that shells out is not, and shelling out is the normal path for a coding run.This means the fix is narrower than "add scoping": the scoping model already exists and is correct. What is missing is applying it to the credentials the runner hands to git and
gh. Issuing the per-instance GitHub App installation token described above and using it for shell git operations would close the gap and make the two layers agree.Also worth noting the asymmetry is currently backwards from a usability standpoint: the contained layer blocks legitimate cleanup (commenting to explain a mistake), while the uncontained layer permits the mistake itself.
fix(coding): a run that writes to a repository it is not registered f…
serge-ivo commentedon Aug 16, 2026
Partial delivery in
7d246958— leaving this issue open. What shipped is detect-and-halt; the issue asks for containment, and containment is not done.What now happens
A remote write (
pr.open,pr.merge,push*,branch.delete,repo.delete,release.publish) whose command names a repository outside the instance's registered set now halts the run, writes anact.out_of_scopeevent aterrorlevel, and raises aneeds_humanboard card titled with the refused repo.The stop reason (item 4) leads with the target:
It also lands in the run's closing report, because #676's complaint is that the summary said "the PAGS platform repo" and nothing contradicted it.
This would have caught the reported run. Its literal command, from the
act.consequentialrow for sessioncsess_f686f1ffat2026-08-16T01:55:44Z, carries--repo:Item 5:
coding_diagnostics→githubAppnow reportswriteScope(the registered slugs),enforcement, and anenforcementNotestating that the first write still lands.What it does NOT prevent — please read this part
A wrong-repo write is still possible. The Engine runs
claude --dangerously-skip-permissionson your machine with your credentials, so the first write lands and is then stopped and named. Measured, not inferred:git@github-personal:…— ssh, so no token can gate agit push;ghis authed from the keyring with account-widerepo+admin:org;GH_TOKENfor the engine (grep: zero injection sites);GH_CONFIG_DIR=~/.config/gh-personal, so it can steer its own credential.Two further honest gaps:
git push -u origin feat/xis the ordinary shape; the repo would have to be inferred from a working directory the act record does not carry. Guessing would halt working runs, so it permits. A cwd-inferredgh pr create --filltherefore still slips through.Items 1–3, and why the credential layer is only half-done
One genuine narrowing shipped:
resolveCloneCredentialnow mints a token GitHub itself restricts to the single repo being cloned (repoScopedInstallationToken, never cached — the installation cache is one slot shared withgithub-issues.tsand the connectors, so a scoped token written there would narrow every reader). That matters because the credential is embedded in the checkout'sorigin, so a managed clone previously carried an org-wide write token.But the engine does not use that credential, so items 1–2 are not met for the case that actually bit.
Item 2 and item 3 are also in direct tension through
gh: a process holds exactly oneghtoken, so a repo-scopedGH_TOKENwould break the cross-repo read you asked to protect. That is not speculative — the read you cited was the Engine running, oncsess_42bdfe20:Resolving that needs a per-invocation credential chooser. Filed as the follow-up: #679
serge-ivo commentedon Aug 16, 2026
Finding: MCP tool layer is correctly scoped per instance; shell layer is not.
The MCP tool layer IS correctly scoped per instance —
github_create_commentfrom the PAS-registered instance against ProAgentStore/platform returned 403, correctly refused. But the shell layer is not: from that same instance,git pushandgh pr createagainst this repo both succeeded, which is how PR #675 exists.The scoping model already works; it is just not applied to the credentials given to git and gh on the runner. The asymmetry is backwards — the contained layer blocked legitimate cleanup while the uncontained layer permitted the mistake.
Recommendation: Use per-instance GitHub App installation tokens with WRITE scoped to the registered repo only, so wrong-repo pushes fail at the credential layer. READ can remain broad since cross-repo reads are useful (one run consulted PAS PR #138 as a reference). Additionally: surface the refused target repo in the run stop reason, and expose effective token scope in
coding_diagnostics.fix(ci): the sweep's two queries move out of coding-store.ts, which #698
fix(runner): a handoff whose page was closed is lost, not solved — th…
serge-ivo commentedon Aug 18, 2026
Audit — still LIVE (containment), and a backlog question for the owner
Re-verified: the detect-and-halt half shipped in
7d246958and the prevention half has not moved.grep -rni "credential\.helper\|GIT_CONFIG_COUNT\|ASKPASS\|insteadOf\|GIT_SSH_COMMAND" packages/browser-runner/src workers/api/src→ 0, andpackages/browser-runner/src/coding/headless.ts:432still spawns withenv: mergeEnv(process.env, this.config.env)— the machine env inherited whole.coding_diagnosticsreportsenforcement: "acts-observed-halt"with the note that the first write still lands, which is accurate.The question: after
7d246958, everything remaining on this issue is what #679 describes, in more detail and with the design tension worked through (the read-broad/write-scoped conflict, thePATH-shim sketch, the four named risks, and the SSH problem that may forcegh-only scope). Two open tickets for one remaining piece of work is the backlog defect this audit is meant to catch.I would close this one as delivered-to-its-shipped-scope and let #679 carry the remainder, because #679 is the actionable of the two. I have not done it, because this issue's own text asks for containment and closing it on a partial delivery is the owner's call, not mine.
If you would rather keep #676 open as the parent, the honest form is to retitle it to the containment goal and let #679 be its one child — but say which, because right now a reader picking either up will do the same work.
One thing on this issue that #679 does NOT carry, and which should survive whichever way you go: the asymmetry finding in the second comment —
github_create_commentcorrectly 403s cross-repo whilegh pr createsucceeds, so the contained layer blocks legitimate cleanup while the uncontained layer permits the mistake. That is the sharpest argument for the fix and it should not be lost in a close.Leaving open and unlabelled-changed pending that decision.
serge-ivo commentedon Aug 18, 2026
Folded into #679, which carries the containment work in more detail.
The detection half of this issue shipped — a write naming a repository outside the registered list halts the run and is recorded. What remains is scoping the credential so the write cannot land in the first place, and that is #679's entire subject.
The asymmetry recorded here must not be lost, so it is restated on #679:
coding_diagnosticsreportsenforcement: "acts-observed-halt"— "The first such write still LANDS — the engine uses this machine's own git and gh credentials, which are not scoped." Observing a wrong-repo push after it has happened is not preventing it.Closing to stop two tickets briefing the same work, per the audit's recommendation. #679 is labelled
P1: blocks external usersand is the only P1 whose blast radius lands outside this account.feat(coder): a `gh` write to an unregistered repo is refused before i…