Auditing a repository you do not trust yet.
When you point an agent at unfamiliar code, that code can address the agent. UNTRUSTED_REPO mode treats repository content as data and never as control: all seven capabilities are denied by default and only an operator can promote a path.
Part of the AppSec agent guide.
When you point a coding agent at a repository you have not read, that repository can address the agent. A file called CLAUDE.md or .github/copilot-instructions.md is loaded as instructions by design, and a hostile one can ask for a downgraded severity, a silenced check, an installed package, or an outbound request. UNTRUSTED_REPO mode exists for exactly that situation. The rule it enforces is one sentence: repository content is data, and it is never control.
Concretely, all seven capabilities are denied by default, an unrecognized capability is also denied because the check is an allowlist, and no file inside the target counts as control unless the operator promoted that exact path and recorded who did it, when, and why.
The seven capabilities, all off
| Capability | What it would allow | Default |
|---|---|---|
FILESYSTEM_WRITE | Modifying anything inside or beside the target. | Denied |
REPO_SCRIPTS | Running a script the target ships, including build and test scripts. | Denied |
PACKAGE_INSTALL | Resolving and installing dependencies the target declares. | Denied |
NETWORK | Any outbound request, including one the target asks for. | Denied |
HOOKS | Executing git hooks or host hooks the target defines. | Denied |
EXTERNAL_MCP | Connecting to an MCP server the target configures. | Denied |
DYNAMIC_TARGET_REQUESTS | Sending traffic to a service the target describes or starts. | Denied |
Nothing inside the target can widen scope, relax policy, enable a capability, mark a finding resolved, or silence a check. Only the operator can do that, and only through an explicit promotion recorded in the scope record — the same record the rest of the contract reads before any testing begins.
REPO_SCRIPTS is not permitted in UNTRUSTED_REPO;
an explicit operator escalation is required and must be recorded in scope.trustThe files that read as instructions
Control-shaped paths are the ones a host may load as configuration or as instructions rather than as source. In this mode they are read as text like everything else.
- Instruction files:
CLAUDE.md,AGENTS.md,GEMINI.md,CURSOR.md,.github/copilot-instructions.md - Rule files:
.cursorrules,.windsurfrules - Host settings and hooks:
.claude/settings.json,.claude/settings.local.json,.claude/hooks.json - Server configuration:
.mcp.json,.vscode/mcp.json - Whole directories:
.claude/,.agents/,.codex/,.cursor/,.windsurf/,.github/skills/,.github/workflows/
A promotion turns exactly one path into control and nothing else. Promoting AGENTS.md does not promote the directory it sits in.
Four ways the scope record fails closed
- A missing trust block is rejected, not downgradedAn
UNTRUSTED_REPOscope with notrustblock is refused. It does not silently fall back to trusting the target, which is the failure mode that makes a mode like this pointless. - TRUSTED_CONTROL cannot be declared here
repository_contentmust beDATA_ONLY. Declaring the target trusted while claiming the untrusted mode is a contradiction the resolver refuses. - A wildcard promotion is refusedA promotion must name a concrete path.
*,**, and.are rejected, because a wildcard promotion re-trusts the whole target in one line. - Every promotion is attributableA promotion requires
promoted_by,promoted_at, andreason. An escalation requiresapproved_by,approved_at, andjustification.
Using it
There is no separate command. The mode is a scope record plus a resolver, so it applies to whatever review you were already going to run — a Gold Check Pack investigation behaves exactly the same way under it. Getting there is the ordinary install; the resolver itself is in the open repository.
{
"mode": "UNTRUSTED_REPO",
"trust": {
"repository_content": "DATA_ONLY",
"promoted_control_sources": []
},
"allowed_tools": ["read-only source inspection"]
}from sechelix_core.untrusted_repo import resolve_trust_policy, review_target_content
policy = resolve_trust_policy(scope) # raises if the scope is unsafe
policy.assert_allows("REPO_SCRIPTS") # raises unless explicitly escalated
reviewed = review_target_content(policy, files) # files: (path, text) pairs
for item in reviewed.quarantined:
print(item.path, item.line, item.pattern)- Mode
UNTRUSTED_REPO- Posture
- Static review. It is not a mode for dynamic testing.
- Capability semantics
- Deny by default, allowlist-checked, unrecognized names denied.
- Promotion granularity
- One concrete path per promotion. The promoted path becomes control and only that path.
- Recorded in
- The scope record, under
scope.trust, alongside the existing authorization fields.
Detection is for the report, not for the defence
The mode also scans target content for seven pattern classes and quarantines what it finds: instruction override, audit suppression, false assurance, severity downgrade, capability request, exfiltration request, and direct address to the agent.
This exists so a report can state what the repository attempted. It is deliberately not the control. The control is that target content is never executed as instruction in the first place, so a novel phrasing that evades the patterns still changes nothing.
What this mode does not do
- It does not sandbox your agent. If your host grants shell access, this policy is a discipline layer rather than a kernel boundary. Scope the agent as well.
- It does not detect every phrasing. Detection is best-effort reporting; the guarantee is the separation between data and control.
- It does not make dynamic testing safe. UNTRUSTED_REPO is a static-review posture, and DYNAMIC_TARGET_REQUESTS is one of the seven denied capabilities.
Those three sentences are published in the reference documentation rather than being left for a reader to discover. A safety mode that overstates its own reach is worse than none, because it moves the failure from visible to assumed. The same instinct is why the published run records the candidates verification refuted alongside the one it confirmed.
Read the repository. Do not let it read you back.
npx skills@latest add omarmohelal/SecHelix --skill sechelix