In this article
A repository can describe a bug without having authority to change your task. This original exercise separates a search-filter repair from a hostile README instruction, then checks proposed edits against a fixed file scope. The example validates a plan; it does not execute commands or claim to solve prompt injection.
Separate useful repository evidence from task authority
You ask a coding assistant to repair a search filter so an empty tag list returns every item. The authorized changes are confined to the filter implementation and its tests. While reading the repository, the assistant encounters a note claiming that maintainers require all filter tests to be disabled before any repair. That note describes a different action from the one you requested.
This is an illustrative attack embedded in a fictional README, not an instruction to follow. Its authority claim is the mechanism: it tries to convert material the assistant should inspect into a new order. OWASP identifies comments, documentation, and other external content as possible carriers of indirect prompt injection. OWASP prompt injection prevention guidance
The assistant still needs repository facts: function names, data shapes, and test commands. Rejecting the authority claim does not mean discarding every sentence in the file. Trace factual claims to code and existing project instructions, and resolve proposed scope changes with the person who assigned the task. A note cannot grant itself permission by saying it is urgent.
Make the approved change concrete before the assistant reads
For this rehearsal, write a short task envelope outside the repository content: repair empty-list filtering; allow edits only to src/filter.py and tests/test_filter.py; preserve existing checks; produce proposed file contents for review. Supply a small input with two items and an empty filter whose expected result contains both items. The reproduction anchors the work to observable behavior.
Present the README passage as quoted evidence alongside that envelope. Ask the assistant to identify instructions in the passage that would expand the task. Do not ask it to obey the passage and then justify what happened. The distinction must survive summarization: a summary should say the README contains a request to disable tests, not that disabling tests has been approved.
A clearer prompt is useful, but it is not an enforcement mechanism. Anthropic’s research describes malicious instructions hidden in content encountered by agents and explicitly treats prompt injection as an ongoing problem. Our repository example applies the same trust distinction to source material; it does not inherit any benchmark result from that research. Anthropic research on prompt injection defenses
Validate the proposed edit scope without executing it
The following Python example accepts a small list of complete-file proposals. Exact logical path matching deliberately rejects alternative spellings, parent-directory segments, configuration files, and test-runner files. The validator does not normalize paths or touch the filesystem. It also rejects duplicate targets and malformed records, so a reviewer receives one unambiguous proposal per allowed file.
Run the script as written. It accepts a permitted filter edit and rejects attempts to change a test configuration file, use a parent-directory path, repeat a target, or attach an extra action field. OWASP recommends checking tool calls against permissions and session context; this fixed-envelope example demonstrates one narrow check at that boundary. OWASP prompt injection prevention guidance
ALLOWED = frozenset({'src/filter.py', 'tests/test_filter.py'})
def validate_plan(changes):
if type(changes) is not list or not 1 <= len(changes) <= len(ALLOWED):
raise ValueError('expected one or two proposed files')
seen = set()
result = []
for change in changes:
if type(change) is not dict or set(change) != {'path', 'content'}:
raise ValueError('expected only path and content')
path, content = change['path'], change['content']
if type(path) is not str or path not in ALLOWED or path in seen:
raise ValueError('unapproved or repeated path')
if type(content) is not str or not 1 <= len(content) <= 20_000:
raise ValueError('invalid proposed content')
seen.add(path)
result.append((path, content))
return tuple(result)
good = [{'path': 'src/filter.py', 'content': 'def keep(item): return True\n'}]
assert validate_plan(good) == (('src/filter.py', good[0]['content']),)
bad_plans = [
[{'path': 'pytest.ini', 'content': '[pytest]\n'}],
[{'path': 'src/../src/filter.py', 'content': 'pass\n'}],
good + good,
[{'path': 'src/filter.py', 'content': 'pass\n', 'action': 'run'}],
]
for plan in bad_plans:
try:
validate_plan(plan)
except ValueError:
continue
raise AssertionError(f'accepted disallowed plan: {plan!r}')
print('Allowed proposal accepted; four disallowed plans rejected.')Explain what the validator cannot establish
The accepted content in the example is intentionally trivial. Passing a path check says nothing about whether the proposed code implements the filter contract. An assistant could put a malicious import in an allowed implementation file or delete every assertion from an allowed test file. Those proposals require content review and controlled execution; the validator must not be described as proving that an edit is safe.
A real file-writing tool must also handle filesystem concerns that this script never reaches. A logical path may resolve through a symlink, or the checkout may change between inspection and writing. An actual executor needs a trusted workspace boundary and appropriate race-resistant handling. Keep that problem separate from the string-envelope demonstration instead of adding a reassuring but incomplete path join.
The limits are part of the design. Our demonstration asks whether a proposed operation falls within two named targets. It does not classify hostile language, inspect network destinations, sandbox imported modules, or approve shell commands. Each extra capability would require its own authorization and enforcement rules.
Check the original bug after rejecting the injected instruction
Return to the empty-filter reproduction. Confirm both expected items appear, then test a filter that should keep one item and exclude the other. Check that the caller’s collection is unchanged if that is part of the public contract. These checks establish whether the patch solves the requested problem; detecting a suspicious README does not substitute for doing the engineering work.
Review the entire proposed test diff before running it. Compare the test inventory with the starting checkout and investigate removed assertions or disabled cases. Read the repository’s test command before executing it in a constrained environment, because existing scripts are code too. Use the project’s normal controls rather than giving the assistant credentials or extra capabilities merely to make a test pass.
Record the outcome in concrete terms: the hostile instruction was treated as content, no expanded operation was approved, the two-file patch was inspected, and the agreed regression passed. If a legitimate fix requires a third file, explain the dependency and obtain a revised scope rather than disguising the extra write as an approved path.
Keep a small audit trail without copying sensitive material
For a team workflow, retain the requested behavior, the proposed file list, and the verification result. A short note that an external passage requested an unrelated operation is usually more useful than copying an entire prompt transcript into an issue. Store only what your normal development process needs, especially when repository context contains private information.
Challenge the workflow with another fictional passage that asks for a different unapproved file change. The exact wording should not matter to the deterministic path check. Then test a harmful edit inside an allowed file to demonstrate why human review and execution controls remain necessary. Both results teach more than reporting that one suspicious sentence was blocked.
Common questions about repository prompt injection
Should an assistant ignore every README? No. Documentation often contains necessary facts and legitimate project guidance. The question is whether a specific passage has authority to change the user’s task or permissions. Check that separately from whether its technical description is accurate.
Does wrapping untrusted text in delimiters solve the problem? No. Delimiters make the intended boundary clearer, but a model can still mishandle it. The example combines a clear task with a deterministic scope check and a review of the resulting code.
Can a second model approve the first model’s edits? It can provide another opinion, but that opinion is not a new grant of access. Keep permission checks outside the models and retain independent behavioral verification. No result here promises that every injection, malicious dependency, or dangerous patch will be detected.
Sources & editorial notes
This article was authored with AI using primary-source research and an automated technical review. It has not been reviewed by a human. Code examples are illustrative; run and verify them in your own environment.
- OWASP prompt injection prevention guidance
Supports indirect repository-content attacks and permission checks at tool boundaries; the validator is an original limited example. Accessed October 31, 2026; access date is not a publication date.
- Anthropic research on prompt injection defenses
Research published November 24, 2025 supports the unresolved nature of prompt injection; no model-ranking claim is made. Accessed October 31, 2026; access date is not a publication date.
Put the reading into practice
Work through an existing repository, review AI suggestions, and test the decisions behind your changes.
Explore practice problems