| 1 | --- |
| 2 | title: Secret protection |
| 3 | description: Custom secret patterns, pushing past push protection with a reason, delegated bypass, validity checks, and every place a secret was found. |
| 4 | --- |
| 5 | |
| 6 | Secret scanning finds keys and tokens in what you push and in your |
| 7 | history; [Security](/guides/security/#secret-scanning) covers the |
| 8 | built-in formats, push protection and dismissing alerts. This page covers |
| 9 | what goes further: patterns of your own, getting a blocked push through |
| 10 | with a reason, having owners approve that, and asking a secret's issuer |
| 11 | whether it still works. |
| 12 | |
| 13 | | Feature | Public repositories | Private repositories | |
| 14 | | --- | --- | --- | |
| 15 | | Secret scanning and push protection | Free | Free | |
| 16 | | Bypass with a reason | Free | Free | |
| 17 | | Custom patterns | Free | Security and quality activation | |
| 18 | | Delegated bypass | Free | Security and quality activation | |
| 19 | | Validity checks | Free | Security and quality activation | |
| 20 | |
| 21 | See [What's free and what's paid](/guides/security/pricing/). |
| 22 | |
| 23 | ## The Secret scanning page |
| 24 | |
| 25 | `g1t.sh/<owner>/<project>/security/secret-scanning` lists the |
| 26 | repository's secret alerts. Filter them by: |
| 27 | |
| 28 | | Filter | Values | |
| 29 | | --- | --- | |
| 30 | | State | Open (in the history, or blocked at a push), Dismissed, Fixed | |
| 31 | | Type | Each kind of secret found, custom patterns by name | |
| 32 | | Validity | Active, Inactive, Unknown, No check | |
| 33 | | Bypassed | Bypassed, Not bypassed | |
| 34 | |
| 35 | Each alert has its own page with every place the secret was found (file, |
| 36 | line and commit), what happened to it, its bypass requests, and its |
| 37 | actions: **Dismiss** or **Reopen** (Admin), **Check with its issuer**, |
| 38 | **Fix with g1t** and, for a blocked push, **Bypass**. |
| 39 | |
| 40 | A secret is one alert however many places hold it: the same value found |
| 41 | in another file or another commit adds a location, not an alert. |
| 42 | |
| 43 | ## Custom patterns |
| 44 | |
| 45 | A custom pattern is a secret format of your own, such as your company's |
| 46 | internal API keys. Published, push protection refuses pushes that add a |
| 47 | match and the default branch's history is scanned again for it, exactly as |
| 48 | for the built-in formats. |
| 49 | |
| 50 | A repository's patterns are under **Custom patterns** on its Secret |
| 51 | scanning page; a workspace's, which cover every repository in it, are at |
| 52 | `g1t.sh/<owner>/-/security/patterns`. A repository is scanned with its own |
| 53 | patterns and its workspace's, up to 100 in all. |
| 54 | |
| 55 | To add one: |
| 56 | |
| 57 | 1. Open **New pattern**. |
| 58 | 2. Give it a **Name**, such as `Acme API key`. |
| 59 | 3. Write the **Secret format** as a regular expression: `acme_[a-z0-9]{32}`. |
| 60 | 4. Optionally, say what must come right **before** and **after** the |
| 61 | secret, also as regular expressions. Without them, the secret must start |
| 62 | at the beginning of a line or after a character that is not a letter or |
| 63 | digit, and end the same way. |
| 64 | 5. Add **test strings**, one a line, and **Save as draft**: each test |
| 65 | string shows where the pattern matched. |
| 66 | 6. **Dry run** it: the pattern runs over the default branch (up to 2,000 |
| 67 | files and 20 MB) and lists what it would find, the matches masked. |
| 68 | Nothing is recorded. A workspace's dry run reads up to ten of its |
| 69 | repositories. |
| 70 | 7. **Publish** it. |
| 71 | |
| 72 | Patterns use the [Rust regex syntax](https://docs.rs/regex/latest/regex/#syntax). |
| 73 | Every pattern runs in time linear in the text it reads, so no pattern can |
| 74 | stall a push: there is no look-around and there are no back-references, |
| 75 | which are what make other engines slow on some inputs. A pattern is |
| 76 | refused, with the reason, when it: |
| 77 | |
| 78 | - is longer than 1,000 characters, |
| 79 | - does not compile, or compiles to more than 1 MB, |
| 80 | - matches an empty string. |
| 81 | |
| 82 | Matching is per line: a secret that spans lines is not found. Lines longer |
| 83 | than 4,000 characters (minified code) and lockfiles are skipped, as for the |
| 84 | built-in formats. A line with `g1t:allow-secret` on it is never reported. |
| 85 | |
| 86 | Changing a pattern's format while it is published scans the history again |
| 87 | too. Deleting one keeps the alerts it found. Repository patterns take the |
| 88 | Admin role; workspace patterns, an owner. |
| 89 | |
| 90 | ## Pushing past push protection |
| 91 | |
| 92 | When push protection refuses a push, git's message has a link for each |
| 93 | secret, to its alert: |
| 94 | |
| 95 | ```text |
| 96 | remote: If it is not a real secret (a test fixture), or you will rotate it later: |
| 97 | remote: - add g1t:allow-secret in a comment on its line, or |
| 98 | remote: - bypass it with a reason at https://g1t.sh/acme/rocket/security/secret-scanning/sec_… |
| 99 | remote: A bypass is recorded with your name and reason (or goes to an owner |
| 100 | remote: to approve, if your workspace asks), then the same push goes through. |
| 101 | ``` |
| 102 | |
| 103 | On the alert, choose **Bypass push protection** with a reason: |
| 104 | |
| 105 | | Reason | API value | The alert | |
| 106 | | --- | --- | --- | |
| 107 | | It's a false positive | `false_positive` | Closed as a false positive | |
| 108 | | It's used in tests | `used_in_tests` | Closed as used in tests | |
| 109 | | I'll fix it later | `will_fix_later` | Stays open, to be rotated; it counts as critical once it lands | |
| 110 | |
| 111 | Then push again, unchanged. Anyone with Write may bypass. The bypass is |
| 112 | recorded on the alert (who, when, the reason and your comment) and in the |
| 113 | workspace's [audit log](/guides/audit-log/) as |
| 114 | `secret_scanning.bypass`. |
| 115 | |
| 116 | ### Delegated bypass |
| 117 | |
| 118 | With **Delegated bypass** on, in the workspace's Security settings |
| 119 | (`g1t.sh/<owner>/-/security/settings`), someone who pushed a blocked |
| 120 | secret asks instead: **Ask to bypass** records a request, and the |
| 121 | workspace's owners are told in their [inbox](/guides/inbox/). The |
| 122 | workspace's owners and the repository's admins review requests at |
| 123 | `g1t.sh/<owner>/-/security/bypass-requests`: |
| 124 | |
| 125 | 1. Open the request. |
| 126 | 2. **Approve** or **Deny**, with a comment if you like. |
| 127 | 3. An approved request bypasses push protection as its requester asked; they |
| 128 | are told and push again. |
| 129 | |
| 130 | Nobody reviews their own request. Owners and admins bypass directly, as |
| 131 | without delegation. Requesters can **Cancel request**. Each step is in the |
| 132 | alert's activity and the audit log (`secret_scanning.bypass_requested`, |
| 133 | `secret_scanning.bypass_reviewed`). |
| 134 | |
| 135 | ## Validity checks |
| 136 | |
| 137 | With **Validity checks** on in the workspace's Security settings, g1t asks |
| 138 | a secret's issuer whether it still works, and marks the alert: |
| 139 | |
| 140 | | Validity | Meaning | |
| 141 | | --- | --- | |
| 142 | | Active | The issuer accepted it. Rotate it. | |
| 143 | | Inactive | The issuer refused it: revoked, expired or never real. | |
| 144 | | Unknown | The issuer could not say (an error, a rate limit), or the secret never landed, so it cannot be read again. | |
| 145 | | No check | There is no safe way to ask about this kind of secret. | |
| 146 | |
| 147 | Each check is the issuer's own read-only identity call, made over HTTPS by |
| 148 | the service that read the secret from your repository. The secret goes |
| 149 | nowhere else, and is never stored: |
| 150 | |
| 151 | | Secret | Check | |
| 152 | | --- | --- | |
| 153 | | GitHub token | `GET https://api.github.com/user` | |
| 154 | | GitLab personal access token | `GET https://gitlab.com/api/v4/personal_access_tokens/self` | |
| 155 | | Stripe live key | `GET https://api.stripe.com/v1/balance` (a restricted key without access to it is still active) | |
| 156 | | Slack token | `POST https://slack.com/api/auth.test` | |
| 157 | | npm token | `GET https://registry.npmjs.org/-/whoami` | |
| 158 | | OpenAI API key | `GET https://api.openai.com/v1/models` | |
| 159 | | Anthropic API key | `GET https://api.anthropic.com/v1/models` | |
| 160 | | SendGrid key | `GET https://api.sendgrid.com/v3/scopes` | |
| 161 | |
| 162 | Other formats have no check: an AWS access key cannot be checked without |
| 163 | its secret key, a Slack webhook address would post a message, and the rest |
| 164 | have no read-only identity call. Custom pattern matches are never checked. |
| 165 | |
| 166 | Open alerts are checked again weekly, and on request with **Check with its |
| 167 | issuer** on the alert. |
| 168 | |
| 169 | ## Fix with g1t |
| 170 | |
| 171 | **Fix with g1t** on a secret alert opens an issue for g1t to take the |
| 172 | secret out of the code and read it from configuration instead (an |
| 173 | environment variable, or the repository's |
| 174 | [Actions secrets](/guides/secrets-and-variables/)). Its pull request lands |
| 175 | through your required checks. Taking a secret out of the code does not make |
| 176 | it safe: it is in the history, so whoever owns it still rotates it with its |
| 177 | issuer and marks the alert **Revoked**. The agent's run is charged as |
| 178 | [agent usage](/guides/usage-and-billing/). |
| 179 | |
| 180 | ## API and MCP |
| 181 | |
| 182 | | Route | What it does | Scope | |
| 183 | | --- | --- | --- | |
| 184 | | `GET /repos/{owner}/{name}/secret-scanning/alerts` | Lists alerts; filter with `state`, `secret_type`, `validity`, `bypassed`. | `security:read` | |
| 185 | | `GET /workspaces/{workspace}/secret-scanning/alerts` | The same across a workspace. | `security:read` | |
| 186 | | `GET /repos/{owner}/{name}/secret-scanning/alerts/{id}` | One alert with its locations, activity and requests. | `security:read` | |
| 187 | | `PATCH /repos/{owner}/{name}/secret-scanning/alerts/{id}` | Dismisses (`state` `dismissed`, `reason`, `comment`) or reopens (`state` `open`). Admin. | `security:write` | |
| 188 | | `GET /repos/{owner}/{name}/secret-scanning/alerts/{id}/locations` | Where the secret was found. | `security:read` | |
| 189 | | `POST /repos/{owner}/{name}/secret-scanning/alerts/{id}/bypass` | Bypasses push protection with `reason`, or asks to. | `security:write` | |
| 190 | | `POST /repos/{owner}/{name}/secret-scanning/alerts/{id}/validity` | Checks with the issuer. | `security:write` | |
| 191 | | `GET /workspaces/{workspace}/secret-scanning/bypass-requests` | Bypass requests; filter with `state`, `repo`. | `security:read` | |
| 192 | | `PATCH /workspaces/{workspace}/secret-scanning/bypass-requests/{id}` | `decision` `approve`, `deny` or `cancel`. | `security:write` | |
| 193 | | `GET`, `POST /repos/{owner}/{name}/secret-scanning/custom-patterns` | Lists or creates patterns (also under `/workspaces/{workspace}`). | `security:read`, `security:write` | |
| 194 | | `PATCH`, `DELETE …/custom-patterns/{id}` | Changes, publishes or deletes one. | `security:write` | |
| 195 | | `POST …/custom-patterns/dry-run` | Runs a pattern over the default branch without saving it. | `security:write` | |
| 196 | |
| 197 | Over MCP, the [`security` tool](/reference/mcp/#security) has the actions |
| 198 | `secret_alerts`, `secret_alert`, `update_secret_alert`, `secret_locations`, |
| 199 | `bypass`, `check_validity`, `bypass_requests`, `review_bypass`, `patterns`, |
| 200 | `create_pattern`, `update_pattern`, `delete_pattern` and |
| 201 | `dry_run_pattern`. g1t's own agents never bypass, dismiss or change |
| 202 | patterns. Webhooks: `secret_scanning_alert.created`, `.fixed`, |
| 203 | `.dismissed`, `.reopened`, and `secret_scanning.bypass_requested`, |
| 204 | `secret_scanning.bypass_reviewed`; see [Webhooks](/guides/webhooks/). |