GitHub Copilot Security Best Practices
Most security advice about AI-assisted development is advice about AI. This is advice about code — specifically, about the ways generated code fails that handwritten code does not, and what a working developer does about it.
The distinguishing property is not that generated code is worse. It is that it is confident. A colleague unsure about a cryptographic detail hedges, asks, or leaves a comment. A model produces the same fluent output whether it is right or wrong, and fluency is what your review instincts are calibrated on.
Key takeaways
- Read the premise before the implementation. The expensive failure is correct code solving the wrong problem.
- Verify that every imported package exists. Plausible package names are a real supply-chain attack surface.
- Deterministic checks first. Compile, test, lint and scan before a human or a model reviews.
- Ten categories deserve slower reading. All of them fail silently.
- Never paste a credential into a prompt, whatever the task is about.
The pipeline
Security for AI-assisted code is mostly ordering. Put the checks that are certain before the checks that are probabilistic.
- PromptHuman judgementWhat you asked, and what context was attached.
- GenerationA suggestion. Nothing has happened yet.
- Compile or parseThe cheapest possible check.
- TestsYours, asserting what you meant.
- Lint and typesDeterministic and repeatable.
- Security scanningCode scanning and secret scanning.
- Dependency scanIncluding: does this package exist?
- Copilot reviewA second reader on the diff.
- Human security reviewHuman judgementFor the categories below.
Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.
Every step before Copilot review is deterministic. That order is deliberate: a linter catches its rule every time, a review catches it usually, and spending review attention on something a tool already enforces is waste that dilutes the findings that matter.
Never trust the premise
The most expensive mistake in AI-assisted development is not a bug. It is a correct, well-tested implementation of something you did not ask for.
Asked to “fix the flaky test”, a model will make the test pass. That is not the same as fixing the flakiness, and the resulting change is clean, plausible and wrong in a way no linter detects. Asked to “add rate limiting”, it may add rate limiting that counts the wrong thing — per process rather than per cluster, per endpoint rather than per user — which looks entirely correct until it fails to limit anything.
Verify that packages exist
This one deserves its own section because it is a genuine supply-chain attack surface rather than a quality problem.
Models generate plausible identifiers, and package names are identifiers. A suggestion may import a package that sounds exactly like something that should exist — right ecosystem, right naming convention, right purpose — and does not. Attackers register those names.
How generated code fails
Understanding the failure shapes changes what you look for, and they are consistent enough to enumerate.
Confidently outdated. The most common by a distance. A model has read a great deal of code written over many years, and older patterns are well represented. Generated cryptography, authentication and framework code frequently reflects what was standard several versions ago — a deprecated cipher mode, a session-handling pattern the framework has since replaced, a hashing call with default parameters that were fine in 2018.
The tell is that the code works. Deprecated is not broken; it is worse than current, and nothing fails to alert you.
Locally correct, globally wrong. Each function does what it says. The composition does not do what the system needs — a check applied in one path and not the parallel one, a transaction boundary in the wrong place, a cache that is correct except that nothing invalidates it.
Plausible identifiers. Package names are the dangerous case, but the same mechanism produces configuration keys, environment variable names and API parameters that look exactly right and do not exist. These usually fail loudly. Usually.
Fills in unstated requirements. Asked for something ambiguous, the code resolves the ambiguity silently and proceeds. The resolution is the most likely one across all codebases, which is not the same as the right one for yours.
Optimistic about inputs. Prompts describe the intended case, so generated handlers frequently trust their input. The validation is not wrong; it is absent, and absence is harder to notice in review than error.
Categories that deserve slower reading
Not all code carries equal risk. These are the categories where a plausible wrong answer is most expensive, and they share one property: failure is silent.
Read these more slowly
- Authentication and session handling
- Authorization and access-control logic
- Cryptography and key handling
- Secret loading and rotation
- Infrastructure IAM and network exposure
- SQL and query construction
- Shell scripts and command construction
- Memory-unsafe code
- Data deletion and retention
- Billing and payment logic
A wrong sort order surfaces in testing. A wrong authorisation check surfaces when someone exploits it, possibly years later. That asymmetry, not the difficulty of the code, is what puts these on the list.
Some specifics worth naming:
Authorisation. The common generated failure is checking authentication where authorisation was needed — confirming who someone is rather than whether they may do this particular thing to this particular record. Both look like a guard clause at the top of a function, and only one of them is doing the job you needed.
SQL and query construction. Parameterise. This includes identifiers you believe are internal, because “internal” is a property of today’s call sites.
Shell commands. Command construction from anything that came from outside the program is the same class of problem in a different syntax.
Cryptography. Generated cryptographic code is frequently outdated rather than outright wrong — a deprecated cipher mode, a key size that was fine years ago, a hand-rolled comparison that leaks timing. Prefer your platform’s high-level primitives, and treat anything that constructs its own scheme as a finding rather than a solution.
Infrastructure IAM. Wildcards in actions or resources, and rules opening a port to the world. Cluster 4 covers the Terraform specifics.
Input, output, and the boundaries between
The generic secure-coding rules apply unchanged; what changes is where to look.
Validate at the boundary. Generated request handlers frequently trust their input, because the prompt that produced them described the happy path and nothing else. Ask specifically about the invalid cases.
Encode on output. Context-appropriate encoding — HTML, URL, SQL, shell — is both easy to omit entirely and easy to get subtly wrong.
Fail closed. A generated error path that logs the problem and carries on where it should have refused outright is a common shape, and it appears most often in exactly the place it does most harm — authorisation code, where continuing means permitting.
Do not swallow exceptions. A bare catch that discards the error in a background task is the classic generated silence. It makes the next incident considerably harder to diagnose, because the evidence was thrown away at the moment it was produced.
Review this handler for what happens with invalid input.
For each parameter: what if it is missing, empty, the wrong type, or far larger than expected? Show me the code path each one takes, and tell me where it fails open rather than closed.
Reviewing at volume
The productivity gain is real, and it creates a problem nobody plans for: more code arrives per hour than the review process was designed to absorb.
A team that used to produce four pull requests a day and now produces nine has not doubled its review capacity. What happens next is predictable — reviews get shallower, the shallowness is invisible because nothing fails, and six months later the codebase contains a quantity of lightly-reviewed code nobody can identify.
Four things help.
Keep changes small enough to review properly. This matters more than it did, because the constraint on change size used to be how fast someone could write it. That constraint is gone; the review constraint is not.
Spend attention proportionally. A change to string formatting and a change to the authorisation middleware do not deserve equal reading. Say so explicitly in your review conventions, because the default is to treat every diff the same.
Automate what is automatable, then trust it. Every rule a linter enforces is a rule reviewers can stop looking for. This is the only lever that genuinely increases capacity rather than redistributing it.
Track whether review is still happening. Not review time — review depth. Whether comments are still being written, whether anyone still requests changes, whether approvals arrive faster than someone could have read the diff.
Asking better security questions
A prompt shapes what you get back, and most security-relevant questions get asked too generally to produce anything useful.
“Is this code secure” produces a reassuring paragraph. It is close to worthless — the model has no threat model, no knowledge of what the code guards, and a strong prior toward saying reasonable-sounding things.
These produce something you can act on:
List every path through this function where it returns without having checked the tenant. For each one, show the line and what the caller would receive.
This project requires that every route under /api/ uses the require_tenant dependency. Check src/routes/ and list any route that does not, with the file and line.
Explain what this authorisation check permits and what it denies. Do not tell me whether it is correct — describe the behaviour, and I will decide whether that is what we want.
The pattern across all three: ask it to enumerate or describe, not to judge. Enumeration is checkable against the code in front of you. A verdict is not, and a confident verdict on a security question is precisely the output you should trust least.
Agents and tools change the calculation
Everything above concerns code you review before it runs. Agents act, and an action that has happened cannot be un-reviewed.
The rule from Cluster 7 applies directly: omit the tool rather than forbidding its use. An instruction not to deploy is a sentence a model usually honours; an agent without a deploy tool has no deploy tool.
Secrets, briefly
The full treatment is the next lesson. The rule that prevents most of the damage:
Do not put a credential into a prompt merely because the task involves that
credential. Use API_TOKEN="<REDACTED>" or ${{ secrets.EXAMPLE_TOKEN }} and
describe the shape rather than pasting the value.
Secret scanning and push protection operate on repository content. They do not see what you typed into a chat window, and they recognise the patterns they know — an internal token format or a database connection string can pass both.
A practice that fits in a working day
Everything above, compressed into what a developer actually does. The test of security guidance is whether it survives a deadline, and a list of twenty checks does not.
Before accepting a suggestion, two questions. Does this do what I asked, or something adjacent? Does it import anything new?
Before opening a pull request, three. Have the tests run? Does anything here fall in a high-risk category? Would I be comfortable explaining every line if asked?
In review, one. What is here that the description did not mention?
That is six questions, and they cover most of what goes wrong. The high-risk category check is the one that changes behaviour most, because it converts a general instruction to be careful — which nobody can act on — into a specific decision about whether this particular change needs slower reading.
Where the organisation has to help
Individual practice has limits, and pretending otherwise puts an unfair weight on whoever happens to be at the keyboard.
Deterministic checks in CI. If a rule matters, enforce it. A convention that depends on everyone remembering is a convention that fails on a bad day.
Instruction files that encode real failures. The team’s actual recurring mistakes, written down where both review and generation read them. Code review covers the format.
Required human review for the high-risk categories. Enforced by branch protection, not by hoping.
A named person to ask. Most “is this okay” questions have an answer; what is usually missing is somebody to ask.
Permission to slow down. A developer who believes they will be criticised for taking an extra hour on the authorisation change will not take it. This is a management control, and it is more load-bearing than any tool in this cluster.
What Copilot is good at, security-wise
Worth stating, because a lesson that is only cautions produces the wrong behaviour.
Explaining unfamiliar code. “What does this authorisation check actually permit” is a question it answers well, and one that saves genuine time in a codebase you did not write. Describing existing behaviour is the task it is most reliable at, because the answer is right there in the code.
Enumerating cases. Asking for the input validation cases you might be missing produces a genuinely useful list, even when some entries are irrelevant.
Writing the tests for error paths. Tedious, mechanical, and exactly the thing that gets skipped.
Reviewing your own change before anyone sees it. A private first pass catches the embarrassing category cheaply, and it is the use with the best ratio of value to risk anywhere in this lesson — the output is words, you are the only reader, and nothing lands until you decide it should.
The pattern: it is strong where the work is describing or enumerating, and weak where the work is judging. Use it accordingly.
Licensing and provenance
Adjacent to security, and frequently owned by the same person.
Copilot can be configured to block suggestions matching public code, and where matching is permitted, code referencing shows the URLs of files containing the match and the licence name where one was found. Both are useful. Neither is a licence determination.
Three things worth knowing precisely.
The filter works against an index that refreshes periodically. GitHub documents refreshes every few months, so very recently published code may not be matched at all.
Matches are rare. GitHub reports matching in under one percent of suggestions, which is reassuring about volume and says nothing about the significance of any individual match.
Absence of a reference is not evidence of absence. A suggestion with no code reference has not been certified as original; it has failed to match an index.
A worked example
One concrete case, because the categories above are easier to apply once you have seen the shape.
Asked to add an endpoint returning a user’s invoices, a generated handler arrives. It compiles, it has a test, and the test passes. What is worth checking, in order:
Does it do what was asked? It returns invoices. Does it return this user’s invoices, or all invoices matching a filter that happens to include theirs? These look identical in a diff and differ entirely in consequence.
Is authorisation present, and is it the right kind? A check that the caller is authenticated is not a check that the caller may read this record. This is the single most common generated failure in this shape of code.
What does the error path do? If the lookup fails, does it return an empty list or an error? An empty list on failure is a silent bug that looks like a legitimate result.
Does the test assert the boundary? A test that fetches one user’s invoices and asserts a count proves nothing about whether another user’s would also come back. The test to look for is the negative one, and it is the one that is usually absent.
Anything imported that is new? One line, five seconds, and it is the check that catches the supply-chain case.
Common questions
Is generated code less secure than handwritten code? The useful question is different: it is produced faster, in larger volume, and with uniform confidence, so the review process has to absorb more without the natural signals that a human author was unsure.
Does Copilot introduce vulnerabilities deliberately? No. The realistic risks are plausible-but-wrong output, outdated patterns, and non-existent dependencies.
Do we still need SAST and code scanning? Yes, and arguably more than before. Deterministic scanners catch their patterns every time; a review catches them usually. As the volume of code rises, the every-time property is what scales.
How do we know if our practice is working? Not from the absence of incidents, which is too slow a signal. Better proxies: whether review comments are still being written, whether anyone has caught a hallucinated dependency recently, and whether people can name the high-risk categories without looking them up.
What about code an agent wrote unattended? The same categories apply and the premise check matters more, because nobody watched the reasoning. Read the agent’s summary against the original request first — Cluster 7 covers reviewing delegated work.
Should we ban Copilot for security-critical code? Bans of this shape tend to push usage somewhere unobservable. Requiring human security review for the categories listed above is more effective and more enforceable.
Teaching this to a team
The content above becomes practice only if people encounter it more than once, and a single onboarding session is not more than once.
Put the rules where the work is. The instructions file is read by review and by generation, so a rule written there shapes behaviour without anyone remembering it. The credential rule and the three or four patterns your codebase gets wrong belong there before they belong in a wiki.
Use real incidents. “We leaked connections twice by opening sessions outside a context manager” changes behaviour. “Follow resource management best practices” does not. The specifics are what people retain.
Review out loud occasionally. Walking through a real pull request in a team setting — what you looked at first, what you skipped, why — transfers judgement in a way no document does, and it surfaces the fact that experienced reviewers do not read diffs top to bottom.
Normalise saying “I do not understand this”. The most dangerous review outcome is an approval given because the reviewer could not follow the code and did not want to say so. Generated code makes this more common, because it is fluent enough to feel like something one ought to follow.
Next
Is Copilot safe for private repositories? takes the question that every security review asks and answers it properly — which means neither yes nor no.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.Sync across devices if you want it everywhere.
Saved in this browser and synced to your account, so it follows you between devices. Manage or delete it.
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.