Skip to content

A coding instance can push to and open PRs on any repo the user can reach, not just its registered one #676

Closed

Description

@proagentstore

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_list returns that single entry and nothing else.
  • It was given an objective to implement proappstore-online/platform issue 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-ticket to ProAgentStore/platform and opened PR feat(mcp): a board ticket can be edited, not only filed and moved #675 there. A different GitHub organisation entirely.
  • proappstore-online/platform received 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:

  1. Issue each coding instance a GitHub App installation token scoped to its registered repo, and use that token for the runner's git and gh operations instead of the account-wide credential. githubApp already shows as configured in coding_diagnostics, so the mechanism is present.
  2. 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.
  3. 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/platform PR 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.
  4. 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.
  5. 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

  1. proagentstore commented on Aug 16, 2026

    @proagentstore
    Author

    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 to proappstore-online/platform):

    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.

  2. serge-ivo commented on Aug 16, 2026

    @serge-ivo
    Contributor

    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 an act.out_of_scope event at error level, and raises a needs_human board card titled with the refused repo.

    The stop reason (item 4) leads with the target:

    Attempted write to "ProAgentStore/platform", not permitted: this agent is registered for proappstore-online/platform. The agent opened a pull request (outcome not observed).

    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.consequential row for session csess_f686f1ff at 2026-08-16T01:55:44Z, carries --repo:

    gh pr create --repo ProAgentStore/platform --base main --head feat/update-board-ticket
    

    Item 5: coding_diagnostics → githubApp now reports writeScope (the registered slugs), enforcement, and an enforcementNote stating 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-permissions on your machine with your credentials, so the first write lands and is then stopped and named. Measured, not inferred:

    • both remotes are git@github-personal:… — ssh, so no token can gate a git push;
    • gh is authed from the keyring with account-wide repo + admin:org;
    • nothing has ever injected GH_TOKEN for the engine (grep: zero injection sites);
    • the Engine sometimes exports its own GH_CONFIG_DIR=~/.config/gh-personal, so it can steer its own credential.

    Two further honest gaps:

    1. A command that names no repository is not judged. git push -u origin feat/x is 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-inferred gh pr create --fill therefore still slips through.
    2. Only a stream-json engine (Claude Code) reports acts. On Codex/Grok this sees nothing at all — same limit as A delegated run records its outcome but not its consequential acts — merging to main is invisible #294/[safety] Nothing decides whether an agent may merge to main — three unattended merges already happened #314.

    Items 1–3, and why the credential layer is only half-done

    One genuine narrowing shipped: resolveCloneCredential now mints a token GitHub itself restricts to the single repo being cloned (repoScopedInstallationToken, never cached — the installation cache is one slot shared with github-issues.ts and the connectors, so a scoped token written there would narrow every reader). That matters because the credential is embedded in the checkout's origin, 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 one gh token, so a repo-scoped GH_TOKEN would break the cross-repo read you asked to protect. That is not speculative — the read you cited was the Engine running, on csess_42bdfe20:

    gh pr view 138 --repo proappstore-online/platform --json ...
    

    Resolving that needs a per-invocation credential chooser. Filed as the follow-up: #679

  3. serge-ivo commented on Aug 16, 2026

    @serge-ivo
    Contributor

    Finding: MCP tool layer is correctly scoped per instance; shell layer is not.

    The MCP tool layer IS correctly scoped per instance — github_create_comment from the PAS-registered instance against ProAgentStore/platform returned 403, correctly refused. But the shell layer is not: from that same instance, git push and gh pr create against 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.

  4. added
    P1: blocks external usersMust be true before someone who is not the owner can run an agent (#68)
    bugSomething isn't working
    securitySecurity hardening / audit finding
    backendBackend / Worker / API work
    runnerpackages/browser-runner + packages/cli — ships only via an npm publish
    coderThe Coder wedge agent (#68) — Engine, Pilot, Co-pilot, Loop, Overseer
    on Aug 18, 2026
  5. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    Contributor

    Audit — still LIVE (containment), and a backlog question for the owner

    Re-verified: the detect-and-halt half shipped in 7d246958 and 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, and packages/browser-runner/src/coding/headless.ts:432 still spawns with env: mergeEnv(process.env, this.config.env) — the machine env inherited whole. coding_diagnostics reports enforcement: "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, the PATH-shim sketch, the four named risks, and the SSH problem that may force gh-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_comment correctly 403s cross-repo while gh pr create succeeds, 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.

  6. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    Contributor

    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_diagnostics reports enforcement: "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 users and is the only P1 whose blast radius lands outside this account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1: blocks external usersMust be true before someone who is not the owner can run an agent (#68)backendBackend / Worker / API workbugSomething isn't workingcoderThe Coder wedge agent (#68) — Engine, Pilot, Co-pilot, Loop, Overseerrunnerpackages/browser-runner + packages/cli — ships only via an npm publishsecuritySecurity hardening / audit finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions