| 1 | --- |
| 2 | title: Security |
| 3 | description: How g1t keeps secrets out of your repositories, finds vulnerable dependencies, and opens the pull requests that fix them. |
| 4 | --- |
| 5 | |
| 6 | import Aside from '../../../components/Aside.astro'; |
| 7 | |
| 8 | Every project has a **Security** page at `g1t.sh/<owner>/<project>/security`. |
| 9 | It shows what g1t has found, and what is being done about each alert: |
| 10 | |
| 11 | - **Secrets.** A push that adds a key or a token is refused before it lands, |
| 12 | and each repository's history is scanned once, in the background. |
| 13 | - **Dependencies.** Every package your lockfiles resolve is checked against |
| 14 | the [OSV](https://osv.dev) database of known vulnerabilities. For each |
| 15 | vulnerable package that has a fix, g1t opens a pull request that raises it |
| 16 | to the fixed version, and the pull request lands through your branch's |
| 17 | required checks and merge queue. |
| 18 | |
| 19 | Alerts are the workspace's to fix, so the Security page needs a |
| 20 | [role](/guides/access-and-roles/) on the repository, whether the project |
| 21 | is public or private: |
| 22 | |
| 23 | | | Needs | |
| 24 | | --- | --- | |
| 25 | | See alerts, **Re-scan now** | Write | |
| 26 | | **Dismiss** or **Reopen** a dependency alert | Write | |
| 27 | | **Security updates** on or off | Maintain | |
| 28 | | **Dismiss** or **Reopen** a secret alert | Admin | |
| 29 | |
| 30 | Someone with Read or Triage is told the page needs Write. The workspace's |
| 31 | own page, `g1t.sh/<owner>/-/security`, lists the open alerts of every |
| 32 | project the member can see them on, most severe first. |
| 33 | |
| 34 | ## The overview |
| 35 | |
| 36 | The top of the page counts open alerts by severity: critical, high, |
| 37 | medium, low and unrated. The counts are: |
| 38 | |
| 39 | - each open vulnerability, by its severity, and |
| 40 | - each secret in the repository's history that looks real, as critical. |
| 41 | |
| 42 | A secret that a push carried but that never landed (push blocked) is not |
| 43 | counted, and neither is a [likely test value](#likely-test-values). A |
| 44 | dismissed alert is not counted. Below the counts: |
| 45 | |
| 46 | - **Re-scan now** reads the dependencies again at once and scans the |
| 47 | history again from the start. |
| 48 | - **Security updates** turns [security updates](#security-updates) on or off |
| 49 | for the project. It is on unless someone turns it off. |
| 50 | |
| 51 | The **Secrets** and **Dependencies** tabs list each alert. Filter either |
| 52 | tab by state: |
| 53 | |
| 54 | | State | Meaning | |
| 55 | | --- | --- | |
| 56 | | Open | Needs attention. | |
| 57 | | Dismissed | Someone dismissed it with a reason. | |
| 58 | | Fixed | The secret was revoked, or the vulnerable version is no longer resolved by any lockfile. | |
| 59 | |
| 60 | ## Secret scanning |
| 61 | |
| 62 | g1t looks for credentials whose format their issuer made recognisable, so a |
| 63 | match is nearly always a real secret or a fake made to look like one: |
| 64 | |
| 65 | | What | What it looks like | |
| 66 | | --- | --- | |
| 67 | | AWS access keys | `AKIA`, `ASIA`, `ABIA` or `ACCA` and 16 more characters | |
| 68 | | AWS secret access keys | 40 characters on a line that names an AWS secret | |
| 69 | | GitHub tokens | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_` | |
| 70 | | GitLab tokens | `glpat-`, `gloas-`, `glrt-`, `glptt-`, `gldt-` | |
| 71 | | Stripe live keys | `sk_live_`, `rk_live_` (test keys are left alone) | |
| 72 | | Slack tokens | `xoxb-`, `xoxp-`, `xoxa-`, `xoxr-`, `xoxs-`, `xoxe-` | |
| 73 | | Slack webhooks | `https://hooks.slack.com/services/` addresses | |
| 74 | | Google API keys | `AIza` and 35 more characters | |
| 75 | | Anthropic API keys | `sk-ant-` | |
| 76 | | OpenAI API keys | `sk-proj-`, `sk-svcacct-`, `sk-admin-`, and older `sk-` keys | |
| 77 | | Private keys | A PEM `BEGIN … PRIVATE KEY` header followed by the key | |
| 78 | | Service-role JWTs | A JWT whose claims carry `service_role` | |
| 79 | | npm tokens | `npm_` | |
| 80 | | g1t tokens | `g1t_` and 40 hex characters | |
| 81 | | SendGrid keys | `SG.` and two dotted parts | |
| 82 | |
| 83 | Placeholders made of a handful of characters, such as `ghp_xxxxxxxx…`, are |
| 84 | not reported. Lockfiles, and files under `node_modules/` and `vendor/`, are |
| 85 | not scanned. |
| 86 | |
| 87 | g1t never stores a secret it finds. It keeps a fingerprint, so it can |
| 88 | recognise the same secret again, and a short preview, such as `AKIA…`, so |
| 89 | you can recognise it. |
| 90 | |
| 91 | ### Likely test values |
| 92 | |
| 93 | g1t judges each value it finds by the value alone, never by the file it is |
| 94 | in: a key under `tests/` is as real as one under `src/` if it was ever |
| 95 | issued. A value is a **likely test value** when it: |
| 96 | |
| 97 | - is a key its issuer publishes as an example, such as AWS's documented |
| 98 | example keys; |
| 99 | - says it is an example: it contains `example`, `sample`, `dummy`, `fake`, |
| 100 | `placeholder`, `changeme`, `notreal`, `redacted` or `xxxxx`; |
| 101 | - counts up for six or more characters in a row, like `abcdef` or `123456`; |
| 102 | - repeats one character five or more times in a row; |
| 103 | - is a short piece repeated; or |
| 104 | - has too little randomness to be a real key. |
| 105 | |
| 106 | A likely test value is still listed, in its own group on the **Secrets** |
| 107 | tab, with the reason. It never stops a push and is never counted as |
| 108 | critical. Private keys are judged by the words only. |
| 109 | |
| 110 | ### Push protection |
| 111 | |
| 112 | When a push over HTTPS adds a secret, g1t refuses the whole push and nothing |
| 113 | is stored. Only the lines the push adds are checked, so a secret that is |
| 114 | already in the repository does not block every later push to the same file. |
| 115 | This applies to every push, including the pushes g1t and its agents make to |
| 116 | pull requests. |
| 117 | |
| 118 | Very large pushes are scanned after they land, not before. Such a push is |
| 119 | too large to read in full before it is stored, so it goes through, and g1t |
| 120 | then scans every commit it added, on whichever branch, in the |
| 121 | background. A secret found there is an open alert, as if it had been found |
| 122 | in history, and the workspace's owners get an email when one looks real. |
| 123 | To have a large push checked before it lands, push it in parts, as |
| 124 | [Size limits](/guides/git/#size-limits) shows. |
| 125 | |
| 126 | Git lists every secret it found, by file and line, with a link to allow |
| 127 | each one: |
| 128 | |
| 129 | ```text |
| 130 | $ git push |
| 131 | remote: g1t found a secret in this push, so nothing was pushed. |
| 132 | remote: |
| 133 | remote: config/prod.env:3 an AWS access key (commit 4807077) |
| 134 | remote: |
| 135 | remote: Take the secret out of the commit that adds it (git commit --amend, or |
| 136 | remote: git rebase -i for an older commit), rotate it if it was ever real, and |
| 137 | remote: push again. |
| 138 | remote: |
| 139 | remote: If it is not a real secret, such as a test fixture: |
| 140 | remote: - add g1t:allow-secret in a comment on its line, or |
| 141 | remote: - allow it once at https://g1t.sh/acme/rocket/security?tab=secrets&finding=sec_… |
| 142 | remote: Allowing is recorded with your name, then the same push goes through. |
| 143 | To https://g1t.sh/acme/rocket.git |
| 144 | ! [remote rejected] main -> main (secret found: config/prod.env:3 has an AWS access key) |
| 145 | ``` |
| 146 | |
| 147 | To fix a real secret: |
| 148 | |
| 149 | 1. Remove it from the commit that adds it: `git commit --amend` for the last |
| 150 | commit, `git rebase -i` for an older one. |
| 151 | 2. Rotate it with whoever issued it. Once a secret has been on any machine |
| 152 | but yours, treat it as known. |
| 153 | 3. Push again. |
| 154 | |
| 155 | ### Letting a secret through |
| 156 | |
| 157 | A fake key that does not look made up is still reported, on purpose: it |
| 158 | looks exactly like a real one. Two ways to let it through: |
| 159 | |
| 160 | - **Mark the line.** Put `g1t:allow-secret` anywhere on the line, usually in |
| 161 | a comment. The line is never reported, in any push or in history. |
| 162 | |
| 163 | ```ts |
| 164 | const FIXTURE_KEY = "AKIA…"; // g1t:allow-secret |
| 165 | ``` |
| 166 | |
| 167 | - **Dismiss the alert.** Open the link in git's message (or the alert on the |
| 168 | **Secrets** tab), choose **Dismiss** with the reason **False positive** or |
| 169 | **Used in tests**, and push again, unchanged: that secret no longer stops |
| 170 | a push to this project. |
| 171 | |
| 172 | Dismissing a secret alert needs the Admin [role](/guides/access-and-roles/) |
| 173 | on the repository. Someone without it sees the same message and asks |
| 174 | someone who has it. |
| 175 | |
| 176 | ### Secrets in history |
| 177 | |
| 178 | The first time g1t sees a repository (when it is created, on its next push |
| 179 | to the default branch, or when its Security page is first opened) it scans |
| 180 | the default branch's history in the background, a page of commits at a |
| 181 | time, comparing each commit with its first parent. The page shows how far |
| 182 | it has got. Scanning is metered to the workspace like other usage, and it |
| 183 | pauses if the workspace reaches its spending limit. |
| 184 | |
| 185 | A secret found in history is open: it is in the repository, and anyone |
| 186 | who could clone it may have it. Rotate it, then dismiss it as **Revoked**. |
| 187 | Removing it from the code is not enough, since it stays in history. |
| 188 | |
| 189 | ### Dismissing a secret alert |
| 190 | |
| 191 | Choose **Dismiss**, pick a reason, and add a comment if you like (up to 500 |
| 192 | characters): |
| 193 | |
| 194 | | Reason | API value | What happens | |
| 195 | | --- | --- | --- | |
| 196 | | False positive | `false_positive` | Dismissed. Pushes carrying the secret go through. | |
| 197 | | Used in tests | `used_in_tests` | Dismissed. Pushes carrying the secret go through. | |
| 198 | | Revoked | `revoked` | Fixed. The secret was real and has been revoked or rotated. | |
| 199 | | Won't fix | `wont_fix` | Dismissed. Pushes carrying the secret go through. | |
| 200 | |
| 201 | Who dismissed it, when, the reason and the comment are kept in the alert's |
| 202 | activity log. **Reopen** puts the alert back to open; a secret that never |
| 203 | landed goes back to blocking pushes. |
| 204 | |
| 205 | ## Dependencies |
| 206 | |
| 207 | g1t reads these lockfiles on the default branch, up to four directories |
| 208 | deep, skipping `node_modules`, `vendor`, `target`, `dist` and `build`: |
| 209 | |
| 210 | | Ecosystem | Files | |
| 211 | | --- | --- | |
| 212 | | npm | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock` | |
| 213 | | Cargo | `Cargo.lock` | |
| 214 | | Go | `go.mod` (or `go.sum` where there is no `go.mod`) | |
| 215 | | Python | `poetry.lock`, and pinned lines (`name==1.2.3`) in `requirements.txt` | |
| 216 | |
| 217 | It reads them on every push to the default branch and again every day, and |
| 218 | asks OSV about every package at the exact version locked. Each advisory |
| 219 | found is listed with its id (its GHSA id when it has one), severity, the |
| 220 | version that fixes it, and the lockfile that resolves the vulnerable |
| 221 | version. A vulnerability that a later scan no longer finds is marked fixed. |
| 222 | |
| 223 | ### Dismissing a dependency alert |
| 224 | |
| 225 | Dismissing a dependency alert needs the Write role. Pick a reason, and add |
| 226 | a comment if you like (up to 500 characters): |
| 227 | |
| 228 | | Reason | API value | |
| 229 | | --- | --- | |
| 230 | | A fix has already been started | `fix_started` | |
| 231 | | No bandwidth to fix this | `no_bandwidth` | |
| 232 | | Risk is tolerable to this project | `tolerable_risk` | |
| 233 | | This alert is inaccurate or incorrect | `inaccurate` | |
| 234 | | Vulnerable code is not actually used | `not_used` | |
| 235 | |
| 236 | A dismissed vulnerability that a later scan finds again stays dismissed |
| 237 | until someone reopens it. The activity log keeps who dismissed it, when, |
| 238 | the reason and the comment. |
| 239 | |
| 240 | ### When there is no fix yet |
| 241 | |
| 242 | When no patched version has been published, the alert links the advisory |
| 243 | and nothing is opened for it. You can dismiss it as **Risk is tolerable to |
| 244 | this project**. g1t checks your dependencies again every day, fetching |
| 245 | OSV's record for each advisory without a fix again, and opens a security |
| 246 | update as soon as a fix is published. |
| 247 | |
| 248 | ## Security updates |
| 249 | |
| 250 | With **Security updates** on, g1t opens a pull request to upgrade each |
| 251 | vulnerable dependency that has a fix. It lands through your branch's |
| 252 | required checks. |
| 253 | |
| 254 | 1. g1t starts a sandbox, which raises the package to the fixed version in |
| 255 | each lockfile that resolves it, using the ecosystem's own tool. |
| 256 | 2. It commits as `g1t <g1t@users.noreply.g1t.sh>` and pushes a branch named |
| 257 | `g1t/security/<package>-<version>`. |
| 258 | 3. It opens a pull request, authored by `g1t`, that names the advisories it |
| 259 | fixes, the old and new versions, and the lockfiles it changed. |
| 260 | 4. The pull request merges through the branch's |
| 261 | [required status checks](/guides/pull-requests/#required-status-checks) |
| 262 | and the [merge queue](/guides/merge-queue/), like any other. When it |
| 263 | merges, the next scan finds the vulnerability fixed. |
| 264 | |
| 265 | Each alert shows its update's status and links the pull request: |
| 266 | |
| 267 | | Status | Meaning | |
| 268 | | --- | --- | |
| 269 | | Making the change | The sandbox is raising the version. | |
| 270 | | Open | The pull request is open. | |
| 271 | | Merged | The pull request merged. | |
| 272 | | Closed | The pull request was closed without merging. | |
| 273 | | Superseded | A newer security update for the same package replaced it, or the package is no longer vulnerable. | |
| 274 | | Needs code changes | Raising the version was not enough; an agent is working on it. | |
| 275 | | Failed | The update could not be made. | |
| 276 | |
| 277 | A newer security update for the same package closes the older pull request |
| 278 | as superseded. So does the package no longer being vulnerable. |
| 279 | |
| 280 | How each lockfile is changed: |
| 281 | |
| 282 | | Lockfile | How the version is raised | |
| 283 | | --- | --- | |
| 284 | | `package-lock.json` | `npm install --package-lock-only --ignore-scripts` for a direct dependency, keeping its range style; for an indirect one, `npm update`, then an `overrides` entry if that is not enough | |
| 285 | | `pnpm-lock.yaml` | `pnpm update --lockfile-only`, then `pnpm.overrides` if needed | |
| 286 | | `yarn.lock` | `yarn up` (Yarn 2 and later) or `yarn upgrade` (Yarn 1), then `resolutions` if needed | |
| 287 | | `Cargo.lock` | `cargo update -p <package> --precise <version>` | |
| 288 | | `go.mod`, `go.sum` | `go get <module>@<version>`, then `go mod tidy` | |
| 289 | | `poetry.lock` | `poetry add <package>@^<version> --lock`, or `poetry update --lock` for an indirect one | |
| 290 | | `requirements.txt` | The `==` pin is rewritten | |
| 291 | |
| 292 | Install scripts never run. A lockfile the sandbox cannot change this way, |
| 293 | such as a `requirements.txt` that pins with `--hash`, is handled as |
| 294 | [when code has to change](#when-code-has-to-change): if no branch has been |
| 295 | pushed 45 minutes after the update started, g1t gets an issue for it. |
| 296 | |
| 297 | ### When code has to change |
| 298 | |
| 299 | Sometimes raising the version is not enough: the bump fails, or the pull |
| 300 | request's required checks fail because code must change. Then g1t opens an |
| 301 | issue and assigns it to [g1t](/guides/working-with-g1t/). That session |
| 302 | shows as **started by g1t**. This is the only time an agent is involved in |
| 303 | a security update. |
| 304 | |
| 305 | Turn **Security updates** off on the Security page to stop g1t opening |
| 306 | these pull requests for a project. Alerts are still listed. |
| 307 | |
| 308 | ## Version updates |
| 309 | |
| 310 | `.g1t/dependencies.yml` on the default branch says which dependencies to |
| 311 | keep current, how often, how to group them, and which to leave alone. |
| 312 | |
| 313 | <Aside type="note" title="Coming soon"> |
| 314 | g1t reads and checks this file today and shows what it found on the |
| 315 | Security page; it does not open version update pull requests yet. |
| 316 | </Aside> |
| 317 | |
| 318 | ```yaml |
| 319 | version: 1 |
| 320 | updates: |
| 321 | - ecosystem: npm |
| 322 | directory: /web |
| 323 | schedule: |
| 324 | interval: daily |
| 325 | open-pull-requests-limit: 3 |
| 326 | groups: |
| 327 | lint: |
| 328 | patterns: ["eslint*", "@typescript-eslint/*"] |
| 329 | ignore: |
| 330 | - dependency: react |
| 331 | versions: [">=19"] |
| 332 | - dependency: left-pad |
| 333 | - ecosystem: cargo |
| 334 | ``` |
| 335 | |
| 336 | The file has `version: 1` and `updates`, a list of up to 50 entries. Each |
| 337 | entry takes: |
| 338 | |
| 339 | | Key | Value | Default | |
| 340 | | --- | --- | --- | |
| 341 | | `ecosystem` | `npm`, `cargo`, `go` or `pip`. Required. | | |
| 342 | | `directory` | Where the lockfile is, from the repository's root. It must stay inside the repository. | `/` | |
| 343 | | `schedule.interval` | `daily`, `weekly` or `monthly`. | `weekly` | |
| 344 | | `open-pull-requests-limit` | How many version update pull requests can be open at once, from 0 to 20. | `5` | |
| 345 | | `groups` | Group names, each with `patterns`: a list of package names, where `*` matches anything. Matching packages are updated together. | | |
| 346 | | `ignore` | A list of entries, each with a `dependency` pattern and an optional `versions` list of requirements, such as `>=19`. With no `versions`, the dependency is never updated. | | |
| 347 | |
| 348 | The same `ecosystem` and `directory` listed twice, an unknown key and an |
| 349 | unknown value are errors. The Security page shows the error, naming the |
| 350 | entry it is in. |
| 351 | |
| 352 | ## The g1t identity |
| 353 | |
| 354 | Pull requests, issues and merges that g1t makes itself, such as security |
| 355 | updates, are authored by `g1t`, shown with the pixel 1 avatar. `g1t` is not |
| 356 | an account: no one can sign in to it, and the name cannot be registered. |
| 357 | |
| 358 | ## API and MCP |
| 359 | |
| 360 | | Route | What it does | Scope | |
| 361 | | --- | --- | --- | |
| 362 | | `GET /repos/{owner}/{name}/security/alerts` | Lists alerts. Filter with `state` (`open`, `dismissed`, `fixed`) and `kind`. | `repo:read` | |
| 363 | | `POST /repos/{owner}/{name}/security/alerts/{id}/dismiss` | Dismisses an alert, with `reason` and an optional `comment`. | `repo:admin` | |
| 364 | | `POST /repos/{owner}/{name}/security/alerts/{id}/reopen` | Reopens a dismissed alert. | `repo:admin` | |
| 365 | |
| 366 | `reason` is one of the API values in the tables above. Over MCP, the |
| 367 | [`repository` tool](/reference/mcp/#repository) has the actions |
| 368 | `security_alerts`, `dismiss_alert` and `reopen_alert`. The token's scope |
| 369 | does not replace the role: dismissing still needs Admin for a secret and |
| 370 | Write for a dependency. See the [API reference](/reference/api/) for the |
| 371 | fields of each. |