Most static analysers pattern-match on source text. CodeQL does something structurally different: it compiles your code into a relational database describing the program — its syntax, its types, its control flow, its data flow — and then runs queries against that database.
The consequence is the thing worth understanding. A grep-based tool can find a dangerous function call. CodeQL can answer “is there any path by which data an attacker controls reaches this function call?” — which is a question about the program, not about the text.
The short answer
Section titled “The short answer”Two phases. Create a database from the code, then run queries against it.
A query language. QL, a declarative logic language. You do not need to write it to use CodeQL — GitHub ships thousands of maintained queries.
Configuration is per language, and the part that most often goes wrong is the build mode for compiled languages.
The output is SARIF, which lands in code scanning like any other analyser’s results.
Databases
Section titled “Databases”The first phase extracts a database. For an interpreted language this means parsing; for a compiled one it means observing the build, so the extractor sees exactly what the compiler saw.
The database contains a relational representation of the program: every expression, every type, every call, the control flow graph, and the data flow relationships derived from them.
This is why analysis is not instant. Building a database for a large codebase takes minutes, and it is the reason CodeQL can answer questions that require following a value through several functions, across files, into a library and back.
It is also why incomplete extraction is the most consequential failure mode. If the extractor did not see part of your code, that part is not in the database, no query examines it, and the result is zero findings — which is indistinguishable from clean code.
Supported languages
Section titled “Supported languages”Verified against the CodeQL documentation, September 2026. The identifier is what you put in a
workflow’s languages: field.
| Language | Identifier | Notes |
|---|---|---|
| C/C++ | c-cpp | C89 through C23; C++98 through C++23 |
| C# | csharp | Up to C# 14 and .NET 10 |
| Go | go | Up to Go 1.27 |
| Java / Kotlin | java-kotlin | Java 7–26; Kotlin 1.8.0–2.4.1 |
| JavaScript / TypeScript | javascript-typescript | ECMAScript 2022 or lower; TypeScript 2.6–7.0 |
| Python | python | 2.7 and 3.5 through 3.14 |
| Ruby | ruby | Up to 3.3 |
| Rust | rust | Editions 2021 and 2024 |
| Swift | swift | 5.4–6.3; requires macOS runners |
| GitHub Actions | actions | Analyses workflow and action metadata files |
Two entries deserve attention.
actions analyses your workflow files. Given that a workflow change is a change to what runs with
your repository’s credentials, this is one of the highest-value languages to enable and one of the
least enabled. It finds the injection patterns described in
Workflow security.
swift requires macOS runners, which are more expensive. Budget for it rather than discovering it.
The languages not listed are not analysed. PHP and Scala are the two most commonly assumed to be supported and are not.
Build modes
Section titled “Build modes”For compiled languages, how the database gets built is the setting that most often produces silently wrong results.
| Mode | What happens |
|---|---|
none | The database is created without running a build |
autobuild | CodeQL detects and runs the build system |
manual | You supply the build commands in the workflow |
none is the convenient option and it has a documented cost. GitHub’s own guidance is explicit
that creating a database without a build “may produce less accurate results than using autobuild or
manual build steps if the build scripts cannot be queried for dependency information, and dependency
guesses are inaccurate”. Generated code — anything the build produces rather than the repository
containing — is also missed.
autobuild works for conventional projects. It fails on unusual ones, and its failure is a job
failure, which is at least visible.
manual is what you use when autobuild cannot work:
- uses: github/codeql-action/init@v4 with: languages: java-kotlin build-mode: manual
- name: Build run: ./gradlew --no-daemon assemble
- uses: github/codeql-action/analyze@v4The build must actually compile the code you want analysed. A build that skips modules, or that resolves everything from a cache without compiling, produces a database missing exactly the code you were trying to check.
Query suites
Section titled “Query suites”Which queries run against the database determines what you find and how much you have to read.
| Suite | Contents |
|---|---|
default | High-precision security queries |
security-extended | More security queries, including lower-precision ones |
security-and-quality | Extended, plus maintainability and correctness queries |
- uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} queries: security-extendedThe progression to follow is default first, security-extended once the default findings are being
worked, and security-and-quality only if you want code health in the same interface as security —
which dilutes the security signal and is usually the wrong trade for a security queue.
The query language, briefly
Section titled “The query language, briefly”You can use CodeQL productively without writing QL. This section exists so the findings make sense rather than to teach the language.
A query has three clauses:
from /* variable declarations */where /* logical formulas */select /* expressions */The documentation’s own introductory example, which touches no program at all:
from int x, int ywhere x = 6 and y = 7select x * yAnalysing a real program means importing that language’s library, which supplies the classes describing the program:
import pythonThe security queries are built on taint tracking: a configuration declaring what counts as a source of untrusted data, what counts as a dangerous sink, and which operations sanitise a value in between. The engine then finds paths from any source to any sink that are not sanitised.
That structure explains both the strength and the failure modes. A finding is a path, which is why the alert shows you the flow rather than a line. A false positive usually means a sanitiser the query did not model. A false negative usually means a source or a sink nobody declared.
Custom queries and packs
Section titled “Custom queries and packs”Custom queries are worth writing when your codebase has a pattern the standard queries do not model — an internal API that must always be called with a permission check, a deprecated function that must not be reintroduced.
They are distributed as query packs, versioned and published, and referenced from a configuration:
name: Custom configuration
packs: - your-org/security-queries@1.2.3
paths-ignore: - "**/test/**" - "**/vendor/**" - uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.ymlTwo things about paths-ignore worth being deliberate about. It reduces noise from generated and
vendored code, and it also means nothing in those paths is analysed — including a real
vulnerability in vendored code you ship. Exclude for noise, knowingly, not by default.
Pin the pack version. A floating reference means your analysis changes without a change on your side.
The CodeQL CLI
Section titled “The CodeQL CLI”Everything above runs in Actions. The same engine is available as a command-line tool, and there are three situations where running it locally is worth the setup.
Developing a custom query. The edit-run-inspect cycle against a local database takes seconds. The same cycle through a workflow takes minutes and burns Actions minutes.
Reproducing a finding. When an alert looks wrong, running the same query against the same database locally is how you establish whether the query is wrong or your reading of it is.
Security research. Given a database of a codebase, you can ask arbitrary questions of it — “every call to this function where the third argument is not a constant”, “every path from any deserialisation to any file write”. This is a genuinely different activity from reading a scanner’s output, and it is what CodeQL was built for.
The shape of local use:
{/* Build a database from a source tree */}codeql database create ./db --language=python --source-root=.
{/* Run a query suite against it, producing SARIF */}codeql database analyze ./db --format=sarif-latest --output=results.sarifThe CLI is included with GitHub Code Security licensing, and available for public repositories. Its version needs to be reasonably current relative to the query packs you run, because queries use library features that ship with the extractors.
Monorepos and multi-language repositories
Section titled “Monorepos and multi-language repositories”A repository containing several languages, or several independently-built projects, is where CodeQL configuration gets genuinely difficult. Three problems recur.
Analysis time multiplies. Five languages means five database builds. Use a matrix so they run in parallel, and consider whether every language needs analysing on every pull request:
strategy: fail-fast: false matrix: include: - language: javascript-typescript build-mode: none - language: python build-mode: none - language: go build-mode: autobuildPath filters cut both ways. Restricting the workflow to run only when relevant paths change saves time, and it means a pull request touching nothing in those paths produces no analysis — so a required check on that analysis never reports. The resolution is to make the job always run and exit early, so the check reports success rather than not reporting at all. This is the same trap described in Branch protection for security.
A partial build hides code. In a monorepo where autobuild finds one project’s build file and
builds only that, the database covers one project and the report covers the repository. This is the
build-mode problem at scale, and the symptom is a suspiciously fast analysis.
The check worth doing once: compare the number of files the extractor reported against the number of source files you expect. A large gap is the finding.
The threat model
Section titled “The threat model”Threat. A vulnerability introduced in first-party code reaches production, and an attacker reaches it through a path from input they control.
Attack surface. Every source of untrusted data — request parameters, headers, uploaded files, message payloads, environment values in some deployments — and every sink where a value has consequences.
Impact. From nothing to complete compromise, determined by what the sink does and whether the path is reachable in the deployment. The analysis reports the path; the deployment determines the impact.
Control. Analysis on every pull request, so a defect is caught in the change that introduced it; scheduled analysis so existing code meets new queries; custom queries for patterns specific to your codebase; and a merge gate on alert state.
Verification. Introduce a known taint path and confirm it is flagged. Then verify the negative case: check the extractor’s file counts, and confirm that the languages you believe are being analysed actually appear in the analysis log.
The second half of that verification is the one that matters, because CodeQL’s most dangerous failure is silent. A misconfigured analysis produces a green check and an empty alert list — which is exactly what a healthy repository produces.
How CodeQL differs from other analysers
Section titled “How CodeQL differs from other analysers”Worth stating, because “we already have a linter with security rules” is a common and reasonable question.
| Pattern-based analysers | CodeQL | |
|---|---|---|
| Operates on | Source text, or a syntax tree | A relational database of the program |
| Typical question | “Does this call look dangerous?” | “Is there a path from untrusted input to this call?” |
| Cross-file reasoning | Limited or none | Yes — flow is followed across functions and files |
| Speed | Seconds | Minutes |
| False positives | Higher, because context is unavailable | Lower for security queries, because the path is proven |
| Extensibility | Rules in a config format | A query language with a program model |
| Cost | Usually free | Free on public repositories; licensed on private |
The practical position is that they are complementary rather than competing. A fast linter with security rules belongs in the pull request check because it runs in seconds. CodeQL belongs there too where budget allows, and belongs on a schedule regardless.
What a linter cannot do is the thing that matters most: follow a value from an HTTP handler, through three helper functions, into a database call in a different module. That requires the program model, and the program model is what the database is.
Rolling it out
Section titled “Rolling it out”The sequence that avoids the two failure modes — an unread backlog, and a silently broken analysis.
-
Enable default setup on a handful of active repositories. Read what comes back. Do not gate anything yet.
-
Verify coverage before trusting the results. Check the analysis log: which languages ran, how many files were extracted, and whether any warnings appeared. This step is the one that catches the Kotlin case and the partial-build case.
-
Triage the initial findings and calibrate. What proportion are real? That number determines how aggressive you can be about gating.
-
Gate on new alerts only, at
highor above, through a ruleset. -
Move the backlog into a security campaign with an owner and a deadline, separate from the gate.
-
Add the scheduled full run at
security-extended. -
Enable the
actionslanguage, which is cheap and covers workflow files. -
Write custom queries for the patterns specific to your codebase, once the standard suites are under control.
Step 2 is the one that gets skipped and the one that determines whether any of the rest means anything.
Reading a finding
Section titled “Reading a finding”CodeQL alerts differ from most analysers’ in that they carry a path, and reading the path is the triage.
-
Look at the source. Is it genuinely attacker-controlled? A “user input” that comes from a config file only an administrator edits is a different risk.
-
Look at the sink. What actually happens with the value.
-
Walk the intermediate steps. The alert shows each hop. This is where a sanitiser the query did not model turns up, and it is the most common legitimate reason to dismiss.
-
Decide whether the path is reachable in practice. The analysis proves the path exists in the code. Whether that code path runs in your deployment is a question the analysis cannot answer.
-
Record the reasoning. The dismissal reason is the only artefact of this work.
Note the asymmetry in step 4. CodeQL is sound about the code and silent about the deployment. A path through a code branch that is disabled by configuration is still a path — and configuration changes.
Where CodeQL stops
Section titled “Where CodeQL stops”Business logic. An authorisation check that permits the wrong access is correct code with the wrong specification. No analyser has the specification.
Anything not in the database. Incomplete extraction, excluded paths, unsupported languages,
generated code missed by build-mode: none.
Frameworks it does not model. Taint tracking depends on the library modelling knowing that a given framework function is a source or a sink. A new or in-house framework is invisible until somebody models it.
Dependencies. CodeQL analyses your code. Vulnerabilities in packages you depend on are Dependabot and dependency review’s territory.
Runtime and configuration. A perfectly analysed application with a misconfigured deployment is a compromised application.
Cost and duration
Section titled “Cost and duration”CodeQL is the slowest check in most pipelines, and the two levers that matter are worth knowing before somebody proposes turning it off.
Run the full analysis on a schedule, and a narrower one on pull requests. A weekly
security-extended run over everything, plus a default-suite pull request analysis, gets most of
the value at a fraction of the cost.
Analyse languages in a matrix with fail-fast: false. Parallel, and one language’s failure does
not discard the others’ results.
What not to do is drop the scheduled run to save time. Push-triggered analysis only ever examines new code; the scheduled run is what re-examines existing code against queries added since — which is how a newly-modelled vulnerability class gets found in code written two years ago.
False positives, and what they usually mean
Section titled “False positives, and what they usually mean”“CodeQL is noisy” is a common complaint and it is usually a diagnosis of one of four specific things. Distinguishing them is what makes tuning possible.
An unmodelled sanitiser. The most common case. Your code validates or escapes the value using a function the query does not know about, so the path looks unbroken. The finding is technically correct — the query cannot see the sanitiser — and the resolution is either a dismissal with a clear reason, or a custom query extension declaring the function as a sanitiser. The second scales; the first does not.
An unreachable path. The code path exists and cannot execute in practice — a branch behind a feature flag that is permanently off, a code path only used by a deleted entry point. Legitimate to dismiss, and worth noting that “permanently off” is a configuration statement that can change.
A source that is not really untrusted. The query treats a value as attacker-controlled and in your system it is not. A configuration value written only by an administrator, for instance. Reasonable, and worth checking that the assumption holds for every deployment rather than the one you are thinking of.
Test code. Findings in test files where the risk does not apply. Handle this with paths-ignore
or a dismissal, and check that the path really is test-only before doing either.
What is not usually happening is the query being wrong about the program. CodeQL’s security queries are heavily used and the analysis is sound about the code it can see. When a finding looks wrong, the gap is nearly always between the code and the deployment, or between the code and a model the query lacks — both of which are facts about your system worth writing down rather than dismissing silently.
Common mistakes
Section titled “Common mistakes”build-mode: none on a Java repository containing Kotlin. The Kotlin is not analysed, and only a
log warning says so.
A manual build that does not build everything. The database is missing the code you cared about, and the result is a clean report.
Assuming a language is supported. PHP and Scala are the usual cases. Check the list.
Broad paths-ignore. Excluding vendor/ silences noise and also silences real findings in code
you ship.
Floating query pack references. Analysis behaviour changes without any change on your side.
No scheduled run. New queries never meet old code.
fail-fast at its default in a language matrix. One build failure discards every language’s
results.
Reading the alert title instead of the path. The path is the evidence, and it is where the sanitiser you forgot about turns up.
Mental model
Section titled “Mental model”CodeQL turns a program into a database and asks it questions. That is why it can trace a value from an HTTP parameter into a database query three files away — and why anything the extractor did not see simply does not exist as far as every query is concerned.
What you learned
Section titled “What you learned”- CodeQL builds a relational database of the program, then runs queries against it
- Incomplete extraction produces zero findings, which looks exactly like clean code
- Language identifiers are specific:
c-cpp,csharp,go,java-kotlin,javascript-typescript,python,ruby,rust,swift,actions - The
actionslanguage analyses workflow files, and is under-enabled relative to its value build-mode: nonetrades accuracy for convenience, and skips Kotlin in a Java repository- Query suites run from
defaulttosecurity-extendedtosecurity-and-quality - Security queries are taint-tracking configurations of sources, sinks and sanitisers
- A finding is a path; the path is what you triage, not the title
paths-ignorereduces noise by removing code from analysis entirely- Scheduled runs are what apply new queries to existing code
Exercise
Section titled “Exercise”Use a disposable public repository in a language you know well.
-
Enable default setup. Note which languages it detected. Predict: did it find everything in the repository?
-
Switch to advanced setup and read the generated workflow — the matrix, the build mode, the
category. -
Add code with a clear taint path: a request parameter concatenated into a shell command. Predict: does the analysis flag it, and does the alert show the path?
-
Add a sanitising function between them that CodeQL is unlikely to model — a custom validator. Predict: does the alert persist? Read the path to see which hop it now takes.
-
Add
paths-ignorecovering that file and re-run. Predict: is the alert dismissed, or does it disappear? These are different outcomes with different records. -
Switch the query suite to
security-extendedand compare finding counts. -
If you have a compiled language available, set
build-mode: noneand thenautobuild, and compare what each finds. -
Delete the repository.