Skip to main content

Glob Pattern and .gitignore Tester API

REAL DATA

Test glob patterns and .gitignore rules against paths — which rule won, at which line, and whether a negation was cancelled by an excluded parent directory.

Authoritative reference data or standards computation

Base URL
/api/glob-match
Capabilities
5 routes
Last updated
July 30, 2026
Data sources · 4
+1 more

GET /api/glob-match/test

Returns the verdict plus a witness: alignment when it matched, failure when it did not, and the steps the bounded matcher spent.

Live requestRuns against the public API
No key required
GET/api/glob-match/test?pattern=src%2F**%2F*.ts&path=src%2Flib%2Futil.ts

Request parameters

The glob pattern, taken exactly as sent: nothing is trimmed, '\' is an escape character and never a path separator. Capped at 512 characters, 64 '/'-separated segments and 256 characters per segment.

The path to test, '/'-separated. May be absolute — an absolute pattern only matches an absolute path. A leading './', repeated '/' and a trailing '/' are normalized away with a warning; '.' and '..' segments are matched literally, never resolved. Capped at 1,024 characters and 64 segments.

Which dialect's rules to apply. `posix` is POSIX.1-2024 XCU 2.14 pattern matching with FNM_PATHNAME semantics: no globstar, no braces, no extglob. `glob` is this endpoint's documented superset — POSIX plus a `**` segment that spans zero or more path segments plus Bash brace expansion, which is what Node tooling implements and is not a published standard. `extglob` adds the five Bash extglob operators.

Whether `*`, `?`, a bracket expression or a `**` segment may match a path segment that starts with a '.'. POSIX.1-2024 XCU 2.14.3 forbids it (that is FNM_PERIOD); `dot=true` lifts the restriction. git has no such rule, so /gitignore always behaves as `dot=true` and takes no `dot` parameter.

`false` compares case-insensitively by folding both sides with Unicode simple lowercase, one code point at a time. It approximates a case-insensitive filesystem; it is not a reproduction of APFS, NTFS or `core.ignoreCase`, and a character whose lowercase is more than one code point is compared unchanged.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/glob-match/test?pattern=src/**/*.ts&path=src/lib/util.ts"
const res = await fetch("https://randomapi.dev/api/glob-match/test?pattern=src/**/*.ts&path=src/lib/util.ts");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/glob-match/test?pattern=src/**/*.ts&path=src/lib/util.ts").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/glob-match/test?pattern=src/**/*.ts&path=src/lib/util.ts"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 5
pattern string required

The glob pattern, taken exactly as sent: nothing is trimmed, '\' is an escape character and never a path separator. Capped at 512 characters, 64 '/'-separated segments and 256 characters per segment.

allowed: 1 – 512
example: pattern=src/**/*.ts
path string required

The path to test, '/'-separated. May be absolute — an absolute pattern only matches an absolute path. A leading './', repeated '/' and a trailing '/' are normalized away with a warning; '.' and '..' segments are matched literally, never resolved. Capped at 1,024 characters and 64 segments.

allowed: 1 – 1024
example: path=src/lib/util.ts
syntax enum

Which dialect's rules to apply. `posix` is POSIX.1-2024 XCU 2.14 pattern matching with FNM_PATHNAME semantics: no globstar, no braces, no extglob. `glob` is this endpoint's documented superset — POSIX plus a `**` segment that spans zero or more path segments plus Bash brace expansion, which is what Node tooling implements and is not a published standard. `extglob` adds the five Bash extglob operators.

default: glob
allowed: posix | glob | extglob
example: syntax=extglob
dot boolean

Whether `*`, `?`, a bracket expression or a `**` segment may match a path segment that starts with a '.'. POSIX.1-2024 XCU 2.14.3 forbids it (that is FNM_PERIOD); `dot=true` lifts the restriction. git has no such rule, so /gitignore always behaves as `dot=true` and takes no `dot` parameter.

default: false
example: dot=true
caseSensitive boolean

`false` compares case-insensitively by folding both sides with Unicode simple lowercase, one code point at a time. It approximates a case-insensitive filesystem; it is not a reproduction of APFS, NTFS or `core.ignoreCase`, and a character whose lowercase is more than one code point is compared unchanged.

default: true
example: caseSensitive=false
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=matched,syntax
exclude list

Return all fields except these (comma-separated).

example: exclude=steps
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 10
matched boolean

Whether the pattern matches the path under the resolved dialect.

example: true

syntax string

The dialect that was applied: posix, glob or extglob.

example: glob

pattern string

The pattern exactly as supplied — never trimmed, never rewritten.

example: src/**/*.ts

path string

The path that was actually matched, after the documented normalization: a leading './' dropped, repeated '/' collapsed and a trailing '/' removed. Each of those changes also emits a warning. Dot segments are never resolved.

example: src/lib/util.ts

matchedPattern string nullable

The brace expansion that matched, first in Bash expansion order. Equal to `pattern` when it contains no braces; null when nothing matched.

example: src/**/*.ts

expandedPatterns string[]

Every brace expansion in Bash left-to-right order — a single entry when the pattern has no braces, or when the dialect has no brace expansion at all.

example: ["src/**/*.ts"]

pathSegments string[]

The path split on '/'. An absolute path yields a leading empty segment, which only a pattern with a leading '/' can match.

example: ["src","lib","util.ts"]

alignment object[] nullable

The witness, non-null only when `matched`: one entry per pattern segment as `{ patternSegment, kind: literal|wildcard|globstar, consumed }`. Every non-globstar entry consumes exactly one path segment, and joining every `consumed` with '/' reproduces `path`. Where several alignments exist, the one that gives each globstar the fewest segments (leftmost globstar first) is returned.

example: [{"patternSegment":"src","kind":"literal","consumed":["src"]},{"patternSegment":"**","kind":"globstar","consumed":["lib"]},{"patternSegment":"*.ts","kind":"wildcard","consumed":["util.ts"]}]

failure object nullable

Non-null only when nothing matched: `{ patternSegmentIndex, patternSegment, pathSegmentsMatched, reason }` — the first pattern segment no prefix of the path can reach, and a one-sentence reason. With braces, it describes `expandedPatterns[0]`.

example: {"patternSegmentIndex":0,"patternSegment":"*.ts","pathSegmentsMatched":0,"reason":"The pattern segment '*.ts' does not match the path segment 'src', and it can never reach across a '/'."}

steps integer

Dynamic-program cell updates this match cost, against the documented budget of 400,000 per request. Published so an adversarial pattern is measurable rather than mysterious.

example: 12

Documented examples

Build-generated requests and complete responses
5
A globstar walks any number of directories
GET /api/glob-match/test?pattern=src/**/*.ts&path=src/lib/util.ts
{
  "data": {
    "matched": true,
    "syntax": "glob",
    "pattern": "src/**/*.ts",
    "path": "src/lib/util.ts",
    "matchedPattern": "src/**/*.ts",
    "expandedPatterns": [
      "src/**/*.ts"
    ],
    "pathSegments": [
      "src",
      "lib",
      "util.ts"
    ],
    "alignment": [
      {
        "patternSegment": "src",
        "kind": "literal",
        "consumed": [
          "src"
        ]
      },
      {
        "patternSegment": "**",
        "kind": "globstar",
        "consumed": [
          "lib"
        ]
      },
      {
        "patternSegment": "*.ts",
        "kind": "wildcard",
        "consumed": [
          "util.ts"
        ]
      }
    ],
    "failure": null,
    "steps": 16"generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A bare *.ts never crosses a slash
GET /api/glob-match/test?pattern=*.ts&path=src/lib/util.ts
{
  "data": {
    "matched": false,
    "syntax": "glob",
    "pattern": "*.ts",
    "path": "src/lib/util.ts",
    "matchedPattern": null,
    "expandedPatterns": [
      "*.ts"
    ],
    "pathSegments": [
      "src",
      "lib",
      "util.ts"
    ],
    "alignment": null,
    "failure": {
      "patternSegmentIndex": 0,
      "patternSegment": "*.ts",
      "pathSegmentsMatched": 0,
      "reason": "The pattern segment '*.ts' does not match the path segment 'src', and it can never reach across a '/'."
    },
    "steps": 4
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/test",
    "params": {
      "pattern": "*.ts",
      "path": "src/lib/util.ts",
      "syntax": "glob",
      "dot": false,
      "caseSensitive": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
POSIX has no globstar, so ** is just *
GET /api/glob-match/test?pattern=src/**/*.ts&path=src/a/b/c.ts&syntax=posix
{
  "data": {
    "matched": false,
    "syntax": "posix",
    "pattern": "src/**/*.ts",
    "path": "src/a/b/c.ts",
    "matchedPattern": null,
    "expandedPatterns": [
      "src/**/*.ts"
    ],
    "pathSegments": [
      "src",
      "a",
      "b",
      "c.ts"
    ],
    "alignment": null,
    "failure": {
      "patternSegmentIndex": 2,
      "patternSegment": "*.ts",
      "pathSegmentsMatched": 2,
      "reason": "The pattern segment '*.ts' does not match the path segment 'b', and it can never reach across a '/'."
    },
    "steps": 8
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/test",
    "params": {
      "pattern": "src/**/*.ts",
      "path": "src/a/b/c.ts",
      "syntax": "posix",
      "dot": false,
      "caseSensitive": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z",
    "warnings": [
      "'**' is not alone in its path segment, so it behaves as a single '*' — gitignore(5) puts it plainly: \"Other consecutive asterisks are considered regular asterisks\".",
      "POSIX.1-2024 defines no globstar, so '**' is two asterisks and behaves exactly as a single '*' — it never crosses a '/'. Pass syntax=glob for zero-or-more-segments matching."
    ]
  }
}
extglob !(…) excludes declaration files
GET /api/glob-match/test?pattern=**/!(*.d).ts&path=src/types.d.ts&syntax=extglob
{
  "data": {
    "matched": false,
    "syntax": "extglob",
    "pattern": "**/!(*.d).ts",
    "path": "src/types.d.ts",
    "matchedPattern": null,
    "expandedPatterns": [
      "**/!(*.d).ts"
    ],
    "pathSegments": [
      "src",
      "types.d.ts"
    ],
    "alignment": null,
    "failure": {
      "patternSegmentIndex": 1,
      "patternSegment": "!(*.d).ts",
      "pathSegmentsMatched": 2,
      "reason": "The path only has 2 segments, so there is nothing left for the pattern segment '!(*.d).ts' to match."
    },
    "steps": 80
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/test",
    "params": {
      "pattern": "**/!(*.d).ts",
      "path": "src/types.d.ts",
      "syntax": "extglob",
      "dot": false,
      "caseSensitive": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
dot=true lets wildcards see a dotfile directory
GET /api/glob-match/test?pattern=**/*.json&path=.config/settings.json&dot=true
{
  "data": {
    "matched": true,
    "syntax": "glob",
    "pattern": "**/*.json",
    "path": ".config/settings.json",
    "matchedPattern": "**/*.json",
    "expandedPatterns": [
      "**/*.json"
    ],
    "pathSegments": [
      ".config",
      "settings.json"
    ],
    "alignment": [
      {
        "patternSegment": "**",
        "kind": "globstar",
        "consumed": [
          ".config"
        ]
      },
      {
        "patternSegment": "*.json",
        "kind": "wildcard",
        "consumed": [
          "settings.json"
        ]
      }
    ],
    "failure": null,
    "steps": 12
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/test",
    "params": {
      "pattern": "**/*.json",
      "path": ".config/settings.json",
      "syntax": "glob",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/glob-match/filter

One row per supplied path. matching genuinely filters the rows, and count is only a limit on what is returned.

Live requestRuns against the public API
No key required
GET/api/glob-match/filter?pattern=src%2F**%2F*.ts&paths=src%2Fa.ts%2Csrc%2Fa%2Fb.ts%2Csrc%2Fa.d.ts%2CREADME.md%2Ctest%2Fa.ts

Request parameters

The glob pattern, taken exactly as sent: nothing is trimmed, '\' is an escape character and never a path separator. Capped at 512 characters, 64 '/'-separated segments and 256 characters per segment.

Comma-separated paths to test — at most 100 entries, 256 characters each and 4,000 characters in total. Each entry is trimmed and empty entries are dropped, so a path containing a comma or a meaningful leading/trailing space cannot be tested here — use /test for that one path.

Which dialect's rules to apply. `posix` is POSIX.1-2024 XCU 2.14 pattern matching with FNM_PATHNAME semantics: no globstar, no braces, no extglob. `glob` is this endpoint's documented superset — POSIX plus a `**` segment that spans zero or more path segments plus Bash brace expansion, which is what Node tooling implements and is not a published standard. `extglob` adds the five Bash extglob operators.

Whether `*`, `?`, a bracket expression or a `**` segment may match a path segment that starts with a '.'. POSIX.1-2024 XCU 2.14.3 forbids it (that is FNM_PERIOD); `dot=true` lifts the restriction. git has no such rule, so /gitignore always behaves as `dot=true` and takes no `dot` parameter.

`false` compares case-insensitively by folding both sides with Unicode simple lowercase, one code point at a time. It approximates a case-insensitive filesystem; it is not a reproduction of APFS, NTFS or `core.ignoreCase`, and a character whose lowercase is more than one code point is compared unchanged.

Which rows to return: every path, only the ones that matched, or only the ones that did not. Zero rows is an empty `data` array plus a warning naming the filter — never a silent empty response.

Advanced response options6 options

Limit the number of returned records (1–100). Defaults to all matches.

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/glob-match/filter?pattern=src/**/*.ts&paths=src/a.ts,src/a/b.ts,src/a.d.ts,README.md,test/a.ts"
const res = await fetch("https://randomapi.dev/api/glob-match/filter?pattern=src/**/*.ts&paths=src/a.ts,src/a/b.ts,src/a.d.ts,README.md,test/a.ts");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/glob-match/filter?pattern=src/**/*.ts&paths=src/a.ts,src/a/b.ts,src/a.d.ts,README.md,test/a.ts").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/glob-match/filter?pattern=src/**/*.ts&paths=src/a.ts,src/a/b.ts,src/a.d.ts,README.md,test/a.ts"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 6
pattern string required

The glob pattern, taken exactly as sent: nothing is trimmed, '\' is an escape character and never a path separator. Capped at 512 characters, 64 '/'-separated segments and 256 characters per segment.

allowed: 1 – 512
example: pattern=src/**/*.ts
paths list required

Comma-separated paths to test — at most 100 entries, 256 characters each and 4,000 characters in total. Each entry is trimmed and empty entries are dropped, so a path containing a comma or a meaningful leading/trailing space cannot be tested here — use /test for that one path.

example: paths=src/a.ts,src/a/b.ts,src/a.d.ts,README.md
syntax enum

Which dialect's rules to apply. `posix` is POSIX.1-2024 XCU 2.14 pattern matching with FNM_PATHNAME semantics: no globstar, no braces, no extglob. `glob` is this endpoint's documented superset — POSIX plus a `**` segment that spans zero or more path segments plus Bash brace expansion, which is what Node tooling implements and is not a published standard. `extglob` adds the five Bash extglob operators.

default: glob
allowed: posix | glob | extglob
example: syntax=extglob
dot boolean

Whether `*`, `?`, a bracket expression or a `**` segment may match a path segment that starts with a '.'. POSIX.1-2024 XCU 2.14.3 forbids it (that is FNM_PERIOD); `dot=true` lifts the restriction. git has no such rule, so /gitignore always behaves as `dot=true` and takes no `dot` parameter.

default: false
example: dot=true
caseSensitive boolean

`false` compares case-insensitively by folding both sides with Unicode simple lowercase, one code point at a time. It approximates a case-insensitive filesystem; it is not a reproduction of APFS, NTFS or `core.ignoreCase`, and a character whose lowercase is more than one code point is compared unchanged.

default: true
example: caseSensitive=false
matching enum

Which rows to return: every path, only the ones that matched, or only the ones that did not. Zero rows is an empty `data` array plus a warning naming the filter — never a silent empty response.

default: all
allowed: all | matched | unmatched
example: matching=unmatched
Universal parameters Shared response and formatting options 6
count int

Limit the number of returned records (1–100). Defaults to all matches.

default: all matches
allowed: 1 – 100
example: count=3
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=path,matched
exclude list

Return all fields except these (comma-separated).

example: exclude=failedAtSegment
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 4
path string

The path as matched, after the same normalization /test applies.

example: src/a.ts

matched boolean

Whether the pattern matches this path.

example: true

matchedPattern string nullable

The brace expansion that matched this path, or null when none did.

example: src/**/*.ts

failedAtSegment integer nullable

Index of the first pattern segment no prefix of this path can reach; null when the path matched. Call /test on the path for the full reason and witness.

example: null

Documented examples

Build-generated requests and complete responses
3
Which of these paths does src/**/*.ts select?
GET /api/glob-match/filter?pattern=src/**/*.ts&paths=src/a.ts,src/a/b.ts,src/a.d.ts,README.md,test/a.ts
{
  "data": [
    {
      "path": "src/a.ts",
      "matched": true,
      "matchedPattern": "src/**/*.ts",
      "failedAtSegment": null
    },
    {
      "path": "src/a/b.ts",
      "matched": true,
      "matchedPattern": "src/**/*.ts",
      "failedAtSegment": null
    },
    {
      "path": "src/a.d.ts",
      "matched": true,
      "matchedPattern": "src/**/*.ts",
      "failedAtSegment": null
    },
    {
      "path": "README.md",
      "matched": false,
      "matchedPattern": null,
      "failedAtSegment": 0
    },
    {
      "path": "test/a.ts",
      "matched": false,
      "matchedPattern": null,
      "failedAtSegment": 0
    }
  ],
  "meta": {
    "endpoint": "glob-match",
    "route": "/filter",
    "count": 5,
    "params": {
      "pattern": "src/**/*.ts",
      "paths": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Show only the paths that failed to match
GET /api/glob-match/filter?pattern=**/*.test.ts&paths=a.test.ts,b.ts,src/c.test.ts&matching=unmatched
{
  "data": [
    {
      "path": "b.ts",
      "matched": false,
      "matchedPattern": null,
      "failedAtSegment": 1
    }
  ],
  "meta": {
    "endpoint": "glob-match",
    "route": "/filter",
    "count": 1,
    "params": {
      "pattern": "**/*.test.ts",
      "paths": [
        "a.test.ts",
        "b.ts",
        "src/c.test.ts"
      ],
      "syntax": "glob",
      "dot": false,
      "caseSensitive": true,
      "matching": "unmatched"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A brace group covers two source roots
GET /api/glob-match/filter?pattern={src,test}/**/*.ts&paths=src/a.ts,test/b.ts,docs/c.ts
{
  "data": [
    {
      "path": "src/a.ts",
      "matched": true,
      "matchedPattern": "src/**/*.ts",
      "failedAtSegment": null
    },
    {
      "path": "test/b.ts",
      "matched": true,
      "matchedPattern": "test/**/*.ts",
      "failedAtSegment": null
    },
    {
      "path": "docs/c.ts",
      "matched": false,
      "matchedPattern": null,
      "failedAtSegment": 0
    }
  ],
  "meta": {
    "endpoint": "glob-match",
    "route": "/filter",
    "count": 3,
    "params": {
      "pattern": "{src,test}/**/*.ts",
      "paths": [
        "src/a.ts",
        "test/b.ts",
        "docs/c.ts"
      ],
      "syntax": "glob",
      "dot": false,
      "caseSensitive": true,
      "matching": "all"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/glob-match/gitignore

Evaluates a pasted .gitignore against one repository-relative path with git's last-match-wins precedence, and reports the excluded ancestor directory when a negation cannot take effect.

Live requestRuns against the public API
No key required
GET/api/glob-match/gitignore?rules=build%2F%0A%21build%2Fkeep.txt&path=build%2Fkeep.txt

Request parameters

The .gitignore text itself — never fetched, never truncated. Newlines travel as %0A. A UTF-8 byte order mark is skipped and CRLF line endings are handled, exactly as git does. Capped at 8,000 characters, 500 lines and 500 usable rules, because a truncated rule file gives a wrong verdict.

Repository-root-relative path as git stores it: '/'-separated, no leading '/', no '.' or '..' segments. A trailing '/' is removed and, unless you passed `isDirectory` yourself, sets it to true.

Whether the final component is a directory. It cannot be inferred from the string and it changes the answer: a `build/` rule matches the directory `build` and never a file named `build` (verified against git). Ancestor components are always evaluated as directories.

`false` approximates git's `core.ignoreCase` by folding ASCII bytes only, which is what git's own byte-wise folding does. It is an approximation of a case-insensitive checkout, not a reproduction of one.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/glob-match/gitignore?rules=build/%0A!build/keep.txt&path=build/keep.txt"
const res = await fetch("https://randomapi.dev/api/glob-match/gitignore?rules=build/%0A!build/keep.txt&path=build/keep.txt");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/glob-match/gitignore?rules=build/%0A!build/keep.txt&path=build/keep.txt").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/glob-match/gitignore?rules=build/%0A!build/keep.txt&path=build/keep.txt"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
rules string required

The .gitignore text itself — never fetched, never truncated. Newlines travel as %0A. A UTF-8 byte order mark is skipped and CRLF line endings are handled, exactly as git does. Capped at 8,000 characters, 500 lines and 500 usable rules, because a truncated rule file gives a wrong verdict.

allowed: ≤ 8000
example: rules=build/ !build/keep.txt
path string required

Repository-root-relative path as git stores it: '/'-separated, no leading '/', no '.' or '..' segments. A trailing '/' is removed and, unless you passed `isDirectory` yourself, sets it to true.

allowed: 1 – 1024
example: path=build/keep.txt
isDirectory boolean

Whether the final component is a directory. It cannot be inferred from the string and it changes the answer: a `build/` rule matches the directory `build` and never a file named `build` (verified against git). Ancestor components are always evaluated as directories.

default: false
example: isDirectory=true
caseSensitive boolean

`false` approximates git's `core.ignoreCase` by folding ASCII bytes only, which is what git's own byte-wise folding does. It is an approximation of a case-insensitive checkout, not a reproduction of one.

default: true
example: caseSensitive=false
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=ignored,path
exclude list

Return all fields except these (comma-separated).

example: exclude=steps
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 10
ignored boolean

Whether git would ignore the path: true when the path's own LAST matching rule excludes it, or when any ancestor directory is excluded (git never descends into an excluded directory).

example: true

path string

The normalized repository-relative path that was evaluated.

example: build/keep.txt

isDirectory boolean

The resolved flag. A trailing '/' on `path` sets it to true when you did not pass it explicitly, and `meta.params` echoes the resolved value.

example: false

reason string

Why: `rule` (the path's own last matching rule decided it — that rule may be a negation, in which case `ignored` is false), `parent-directory` (an ancestor is excluded, so git never looks inside), or `no-match` (no rule matches the path or any ancestor).

example: parent-directory

matchedRule object nullable

The rule that decided the verdict: `{ lineNumber, pattern, effectivePattern, negated, anchored, directoryOnly, matchedAgainst, appliedTo }`. `pattern` is the line as written (git's unescaped-trailing-space rule applied); `effectivePattern` is what was actually matched, with the '!', the leading '/' and the trailing '/' removed; `appliedTo` is the path or the ancestor directory the rule matched. Null when nothing matched.

example: {"lineNumber":1,"pattern":"build/","effectivePattern":"build","negated":false,"anchored":false,"directoryOnly":true,"matchedAgainst":"basename","appliedTo":"build"}

competingRules object[]

Every rule that matches THE PATH ITSELF, in file order, in the same shape minus `appliedTo`. gitignore is last-match-wins, so the LAST entry is the path's own decision — the opposite of the robots.txt endpoint, where the first entry wins.

example: [{"lineNumber":1,"pattern":"build/","effectivePattern":"build","negated":false,"anchored":false,"directoryOnly":true,"matchedAgainst":"basename"},{"lineNumber":2,"pattern":"!build/keep.txt","effectivePattern":"build/keep.txt","negated":true,"anchored":true,"directoryOnly":false,"matchedAgainst":"path"}]

excludedByParent object nullable

`{ directory, rule }` naming the shallowest excluded ancestor — the reason a '!' rule can sit in `competingRules` and still lose, which is the single most common .gitignore surprise. Null when no ancestor is excluded.

example: {"directory":"build","rule":{"lineNumber":1,"pattern":"build/","effectivePattern":"build","negated":false,"anchored":false,"directoryOnly":true,"matchedAgainst":"basename"}}

ancestorDecisions object[]

One entry per ancestor directory, root first: `{ directory, ignored, rule }`, where `ignored` is that directory's own last-match verdict evaluated on its own and `rule` is nullable. The shallowest `ignored: true` is the one that decides.

example: [{"directory":"build","ignored":true,"rule":{"lineNumber":1,"pattern":"build/","effectivePattern":"build","negated":false,"anchored":false,"directoryOnly":true,"matchedAgainst":"basename"}}]

ruleCount integer

Usable pattern lines parsed — blank lines, '#' comments and lines that carry no pattern are excluded.

example: 2

steps integer

Dynamic-program cell updates the whole evaluation cost (every rule against the path and against each ancestor), against the documented budget of 400,000 per request.

example: 24

Documented examples

Build-generated requests and complete responses
5
The negation that cannot win
GET /api/glob-match/gitignore?rules=build/%0A!build/keep.txt&path=build/keep.txt
{
  "data": {
    "ignored": true,
    "path": "build/keep.txt",
    "isDirectory": false,
    "reason": "parent-directory",
    "matchedRule": {
      "lineNumber": 1,
      "pattern": "build/",
      "effectivePattern": "build",
      "negated": false,
      "anchored": false,
      "directoryOnly": true,
      "matchedAgainst": "basename",
      "appliedTo": "build"
    },
    "competingRules": [
      {
        "lineNumber": 2,
        "pattern": "!build/keep.txt",
        "effectivePattern": "build/keep.txt",
        "negated": true,
        "anchored": true,
        "directoryOnly": false,
        "matchedAgainst": "path"
      }
    ],
    "excludedByParent": {
      "directory": "build",
      "rule": {
        "lineNumber": 1,
        "pattern": "build/",
        "effectivePattern": "build",
        "negated": false,
        "anchored": false,
        "directoryOnly": true,
        "matchedAgainst": "basename"
      }
    },
    "ancestorDecisions": [
    …
    ]
  }
}
Exclude everything, then re-include one directory
GET /api/glob-match/gitignore?rules=/*%0A!/src&path=src/index.ts
{
  "data": {
    "ignored": false,
    "path": "src/index.ts",
    "isDirectory": false,
    "reason": "no-match",
    "matchedRule": null,
    "competingRules": [],
    "excludedByParent": null,
    "ancestorDecisions": [
      {
        "directory": "src",
        "ignored": false,
        "rule": {
          "lineNumber": 2,
          "pattern": "!/src",
          "effectivePattern": "src",
          "negated": true,
          "anchored": true,
          "directoryOnly": false,
          "matchedAgainst": "path"
        }
      }
    ],
    "ruleCount": 2,
    "steps": 8
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/gitignore",
    "params": {
      "rules": "/*\n!/src",
      "path": "src/index.ts",
      "isDirectory": false,
      "caseSensitive": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A directory-only rule ignores a file of the same name
GET /api/glob-match/gitignore?rules=build/&path=build&isDirectory=false
{
  "data": {
    "ignored": false,
    "path": "build",
    "isDirectory": false,
    "reason": "no-match",
    "matchedRule": null,
    "competingRules": [],
    "excludedByParent": null,
    "ancestorDecisions": [],
    "ruleCount": 1,
    "steps": 0
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/gitignore",
    "params": {
      "rules": "build/",
      "path": "build",
      "isDirectory": false,
      "caseSensitive": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A trailing /** does not match the directory itself
GET /api/glob-match/gitignore?rules=abc/**&path=abc&isDirectory=true
{
  "data": {
    "ignored": false,
    "path": "abc",
    "isDirectory": true,
    "reason": "no-match",
    "matchedRule": null,
    "competingRules": [],
    "excludedByParent": null,
    "ancestorDecisions": [],
    "ruleCount": 1,
    "steps": 4
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/gitignore",
    "params": {
      "rules": "abc/**",
      "path": "abc",
      "isDirectory": true,
      "caseSensitive": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Last match wins, so line 2 re-includes the file
GET /api/glob-match/gitignore?rules=*.log%0A!important.log&path=important.log
{
  "data": {
    "ignored": false,
    "path": "important.log",
    "isDirectory": false,
    "reason": "rule",
    "matchedRule": {
      "lineNumber": 2,
      "pattern": "!important.log",
      "effectivePattern": "important.log",
      "negated": true,
      "anchored": false,
      "directoryOnly": false,
      "matchedAgainst": "basename",
      "appliedTo": "important.log"
    },
    "competingRules": [
      {
        "lineNumber": 1,
        "pattern": "*.log",
        "effectivePattern": "*.log",
        "negated": false,
        "anchored": false,
        "directoryOnly": false,
        "matchedAgainst": "basename"
      },
      {
        "lineNumber": 2,
        "pattern": "!important.log",
        "effectivePattern": "important.log",
        "negated": true,
        "anchored": false,
        "directoryOnly": false,
        "matchedAgainst": "basename"
      }
    ],
    "excludedByParent": null,
    "ancestorDecisions": [],
    "ruleCount": 2,
    "steps": 19"generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/glob-match/explain

Per-segment, per-token explanation for the dialect you name, plus the list of constructs that dialect does not interpret at all. Under a dialect with brace expansion the tokens describe explainedPattern — the first expansion, because that is what gets matched.

Live requestRuns against the public API
No key required
GET/api/glob-match/explain?pattern=src%2F**%2F*.%7Bts%2Ctsx%7D

Request parameters

The glob pattern, taken exactly as sent: nothing is trimmed, '\' is an escape character and never a path separator. Capped at 512 characters, 64 '/'-separated segments and 256 characters per segment.

Which dialect's rules to apply. `posix` is POSIX.1-2024 XCU 2.14 pattern matching with FNM_PATHNAME semantics: no globstar, no braces, no extglob. `glob` is this endpoint's documented superset — POSIX plus a `**` segment that spans zero or more path segments plus Bash brace expansion, which is what Node tooling implements and is not a published standard. `extglob` adds the five Bash extglob operators. `gitignore` reads the text as one .gitignore line: a leading '!' negates, a trailing '/' means directory-only, and matching runs over UTF-8 bytes.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/glob-match/explain?pattern=src/**/*.{ts,tsx}"
const res = await fetch("https://randomapi.dev/api/glob-match/explain?pattern=src/**/*.{ts,tsx}");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/glob-match/explain?pattern=src/**/*.{ts,tsx}").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/glob-match/explain?pattern=src/**/*.{ts,tsx}"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
pattern string required

The glob pattern, taken exactly as sent: nothing is trimmed, '\' is an escape character and never a path separator. Capped at 512 characters, 64 '/'-separated segments and 256 characters per segment.

allowed: 1 – 512
example: pattern=src/**/*.{ts,tsx}
syntax enum

Which dialect's rules to apply. `posix` is POSIX.1-2024 XCU 2.14 pattern matching with FNM_PATHNAME semantics: no globstar, no braces, no extglob. `glob` is this endpoint's documented superset — POSIX plus a `**` segment that spans zero or more path segments plus Bash brace expansion, which is what Node tooling implements and is not a published standard. `extglob` adds the five Bash extglob operators. `gitignore` reads the text as one .gitignore line: a leading '!' negates, a trailing '/' means directory-only, and matching runs over UTF-8 bytes.

default: glob
allowed: posix | glob | extglob | gitignore
example: syntax=gitignore
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=syntax,pattern
exclude list

Return all fields except these (comma-separated).

example: exclude=expandedCount
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 8
syntax string

The dialect the explanation is written for: posix, glob, extglob or gitignore.

example: gitignore

pattern string

The pattern exactly as supplied.

example: !/build/**

explainedPattern string

The pattern `segments` actually describes, which is `pattern` itself unless one of two documented things applies. Under `glob`/`extglob` with a correctly-formed brace group it is the FIRST of the `expandedCount` expansions — the one `/test` tries first — because braces are expanded BEFORE matching, so tokens of the raw text would describe a pattern that is never matched. Under `syntax=gitignore` it is the rule body, identical to `gitignore.effectivePattern`: the leading '!' and the anchoring or directory-only '/' are reported in the `gitignore` object instead of being tokenized. A warning names the reason whenever this differs from `pattern`; `/expand` lists every expansion.

example: build/**

segments object[]

One entry per '/'-separated segment of `explainedPattern`: `{ index, text, kind: literal|wildcard|globstar, tokens }`, where each token is `{ text, kind: literal|escape|any|star|bracket|class|extglob, meaning }`. Every `meaning` is a statement about the dialect named in `syntax`, so it is true of the pattern this endpoint would actually match.

example: [{"index":0,"text":"build","kind":"literal","tokens":[{"text":"build","kind":"literal","meaning":"Matches the literal text 'build'."}]}]

features string[]

Which constructs the dialect actually interprets in this pattern, from a closed vocabulary: globstar, braces, sequence, bracket-expression, character-class, escape, extglob, negation, anchor, directory-only. Collected across every brace expansion, so a bracket that only appears in the second expansion is still listed.

example: ["globstar","negation","anchor","directory-only"]

gitignore object nullable

Non-null only for `syntax=gitignore`: `{ negated, anchored, directoryOnly, matchedAgainst, effectivePattern }` — how git would file this single line.

example: {"negated":true,"anchored":true,"directoryOnly":false,"matchedAgainst":"path","effectivePattern":"build/**"}

unsupported object[]

Every construct present in the text that this dialect does NOT interpret, as `{ text, note }`. This is the honesty engine: it is how you find out that `**` is not a globstar under posix and that `{a,b}` is literal in a .gitignore.

example: [{"text":"{","note":"git has no brace expansion, so '{' and '}' are literal characters in a .gitignore rule."}]

expandedCount integer

How many patterns brace expansion produces — 1 when there are no braces or the dialect has none. Above 1, `explainedPattern` is the first of them.

example: 1

Documented examples

Build-generated requests and complete responses
4
A brace group is expanded before it is explained
GET /api/glob-match/explain?pattern=src/**/*.{ts,tsx}
{
  "data": {
    "syntax": "glob",
    "pattern": "src/**/*.{ts,tsx}",
    "explainedPattern": "src/**/*.ts",
    "segments": [
      {
        "index": 0,
        "text": "src",
        "kind": "literal",
        "tokens": [
          {
            "text": "src",
            "kind": "literal",
            "meaning": "Matches the literal text 'src'."
          }
        ]
      },
      {
        "index": 1,
        "text": "**",
        "kind": "globstar",
        "tokens": [
          {
            "text": "**",
            "kind": "star",
            "meaning": "A '**' alone in a segment spans zero or more whole path segments."
          }
        ]
      },
      {
        "index": 2,
        "text": "*.ts",
        "kind": "wildcard",
        "tokens": [
          {
            "text": "*",
            "kind": "star",
            "meaning": "Matches zero or more characters within one path segment, never a '/'."
          },
    …
    ]
  }
}
How git files a negated, anchored rule
GET /api/glob-match/explain?pattern=!/build/**&syntax=gitignore
{
  "data": {
    "syntax": "gitignore",
    "pattern": "!/build/**",
    "explainedPattern": "build/**",
    "segments": [
      {
        "index": 0,
        "text": "build",
        "kind": "literal",
        "tokens": [
          {
            "text": "build",
            "kind": "literal",
            "meaning": "Matches the literal text 'build'."
          }
        ]
      },
      {
        "index": 1,
        "text": "**",
        "kind": "globstar",
        "tokens": [
          {
            "text": "**",
            "kind": "star",
            "meaning": "A '**' alone in a segment spans zero or more path segments; as the LAST segment of a .gitignore rule it needs at least one, so 'abc/**' does not match the directory 'abc'."
          }
        ]
      }
    ],
    "features": [
      "globstar",
      "negation",
      "anchor"
    ],
    "gitignore": {
      "negated": true,
      "anchored": true,
      "directoryOnly": false,
    …
    ]
  }
}
What an extglob negation group means
GET /api/glob-match/explain?pattern=**/!(*.d).ts&syntax=extglob
{
  "data": {
    "syntax": "extglob",
    "pattern": "**/!(*.d).ts",
    "explainedPattern": "**/!(*.d).ts",
    "segments": [
      {
        "index": 0,
        "text": "**",
        "kind": "globstar",
        "tokens": [
          {
            "text": "**",
            "kind": "star",
            "meaning": "A '**' alone in a segment spans zero or more whole path segments."
          }
        ]
      },
      {
        "index": 1,
        "text": "!(*.d).ts",
        "kind": "wildcard",
        "tokens": [
          {
            "text": "!(*.d)",
            "kind": "extglob",
            "meaning": "Matches anything except one of the given patterns."
          },
          {
            "text": ".ts",
            "kind": "literal",
            "meaning": "Matches the literal text '.ts'."
          }
        ]
      }
    ],
    "features": [
      "globstar",
      "extglob"
    ],
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Everything posix ignores in a modern pattern
GET /api/glob-match/explain?pattern=src/**/!(*.d).{ts,tsx}&syntax=posix
{
  "data": {
    "syntax": "posix",
    "pattern": "src/**/!(*.d).{ts,tsx}",
    "explainedPattern": "src/**/!(*.d).{ts,tsx}",
    "segments": [
      {
        "index": 0,
        "text": "src",
        "kind": "literal",
        "tokens": [
          {
            "text": "src",
            "kind": "literal",
            "meaning": "Matches the literal text 'src'."
          }
        ]
      },
      {
        "index": 1,
        "text": "**",
        "kind": "wildcard",
        "tokens": [
          {
            "text": "**",
            "kind": "star",
            "meaning": "POSIX.1-2024 defines no globstar, so '**' behaves exactly as one '*': zero or more characters within one path segment, never a '/'."
          }
        ]
      },
      {
        "index": 2,
        "text": "!(*.d).{ts,tsx}",
        "kind": "wildcard",
        "tokens": [
          {
            "text": "!(",
            "kind": "literal",
            "meaning": "Matches the literal text '!('."
          },
    …
    ]
  }
}

GET /api/glob-match/expand

Bash 5.3 brace expansion, in left-to-right order, never sorted and never deduplicated. POSIX fnmatch() and git have no brace expansion at all.

Live requestRuns against the public API
No key required
GET/api/glob-match/expand?pattern=src%2F%7Ba%2Cb%7D%2F*.%7Bts%2Ctsx%7D

Request parameters

The pattern to expand, taken exactly as sent. Up to 256 expansions and 32,768 expanded characters — over that is a 400, never a truncated list.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/glob-match/expand?pattern=src/{a,b}/*.{ts,tsx}"
const res = await fetch("https://randomapi.dev/api/glob-match/expand?pattern=src/{a,b}/*.{ts,tsx}");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/glob-match/expand?pattern=src/{a,b}/*.{ts,tsx}").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/glob-match/expand?pattern=src/{a,b}/*.{ts,tsx}"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 1
pattern string required

The pattern to expand, taken exactly as sent. Up to 256 expansions and 32,768 expanded characters — over that is a 400, never a truncated list.

allowed: 1 – 512
example: pattern=src/{a,b}/*.{ts,tsx}
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=pattern,patterns
exclude list

Return all fields except these (comma-separated).

example: exclude=literalGroups
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 5
pattern string

The pattern exactly as supplied.

example: src/{a,b}/*.{ts,tsx}

patterns string[]

The expansions in Bash left-to-right order — never sorted, never deduplicated, so `a{,}b` legitimately yields 'ab' twice. Backslashes are kept: brace expansion does not do quote removal.

example: ["src/a/*.ts","src/a/*.tsx","src/b/*.ts","src/b/*.tsx"]

expandedCount integer

Length of `patterns`, i.e. the product of every group that expanded.

example: 4

braceGroups integer

How many brace groups actually expanded.

example: 2

literalGroups string[]

Brace groups left exactly as written because they are not correctly formed — no unquoted comma and no valid sequence expression, e.g. `{a}`. Bash leaves these alone, and so do we.

example: []

Documented examples

Build-generated requests and complete responses
4
Two comma lists multiply
GET /api/glob-match/expand?pattern=src/{a,b}/*.{ts,tsx}
{
  "data": {
    "pattern": "src/{a,b}/*.{ts,tsx}",
    "patterns": [
      "src/a/*.ts",
      "src/a/*.tsx",
      "src/b/*.ts",
      "src/b/*.tsx"
    ],
    "expandedCount": 4,
    "braceGroups": 2,
    "literalGroups": []
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/expand",
    "params": {
      "pattern": "src/{a,b}/*.{ts,tsx}"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A numeric sequence expression
GET /api/glob-match/expand?pattern=v{1..3}/**/*.json
{
  "data": {
    "pattern": "v{1..3}/**/*.json",
    "patterns": [
      "v1/**/*.json",
      "v2/**/*.json",
      "v3/**/*.json"
    ],
    "expandedCount": 3,
    "braceGroups": 1,
    "literalGroups": []
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/expand",
    "params": {
      "pattern": "v{1..3}/**/*.json"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Zero-padded months, Bash 5 style
GET /api/glob-match/expand?pattern=report-{01..12}.csv
{
  "data": {
    "pattern": "report-{01..12}.csv",
    "patterns": [
      "report-01.csv",
      "report-02.csv",
      "report-03.csv",
      "report-04.csv",
      "report-05.csv",
      "report-06.csv",
      "report-07.csv",
      "report-08.csv",
      "report-09.csv",
      "report-10.csv",
      "report-11.csv",
      "report-12.csv"
    ],
    "expandedCount": 12,
    "braceGroups": 1,
    "literalGroups": []
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/expand",
    "params": {
      "pattern": "report-{01..12}.csv"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
An empty element and a group that is not one
GET /api/glob-match/expand?pattern=x{,y}{a}
{
  "data": {
    "pattern": "x{,y}{a}",
    "patterns": [
      "x{a}",
      "xy{a}"
    ],
    "expandedCount": 2,
    "braceGroups": 1,
    "literalGroups": [
      "{a}"
    ]
  },
  "meta": {
    "endpoint": "glob-match",
    "route": "/expand",
    "params": {
      "pattern": "x{,y}{a}"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z",
    "warnings": [
      "Left as written because they are not correctly-formed brace expansions: {a}."
    ]
  }
}

About this API

Coverage & behavior

Glob and .gitignore matching computed from strings you supply. Nothing is fetched, no filesystem is read and no repository is inspected, which is exactly why this works from CI against a .gitignore that is only in your working tree, and why a coding agent with no shell can still check the ignore rule it just wrote.

/gitignore is the flagship. It answers "would git ignore this path, and which line decided it?" — and, uniquely, it tells you when a ! negation lost to an excluded parent directory. That is the single most common .gitignore surprise: build/ excludes the directory, so git never descends into it and !build/keep.txt on the next line can never take effect. gitignore(5) states the rule plainly — "It is not possible to re-include a file if a parent directory of that file is excluded" — and this endpoint reports it as reason: "parent-directory" with the ancestor and its rule, instead of leaving you to guess. Write build/* instead and the negation works; /gitignore shows the difference in one request.

Remember which way precedence runs: gitignore is last-match-wins, so competingRules is in file order and the last entry is the path's own decision.

/test matches one path against one pattern and returns a witness: alignment shows which pattern segment consumed which path segments, so a match is auditable rather than asserted. When nothing matches, failure names the first pattern segment no prefix of the path can reach. /filter runs up to 100 paths against one pattern in a single request. /explain breaks a pattern into segments and tokens and — the part that matters — lists every construct the dialect you named does not interpret. Because braces are expanded before matching, it explains the first expansion and names it in explainedPattern rather than pretending {ts,tsx} is literal text. /expand shows exactly what Bash brace expansion produces.

Which dialect are you even asking about? Most "glob testers" silently pick one library's behaviour. Here syntax is explicit:

  • posix — POSIX.1-2024 XCU 2.14 with fnmatch()'s FNM_PATHNAME semantics. There is no globstar in POSIX: ** is two asterisks and behaves exactly as one *, so src/**/*.ts matches src/a/b.ts and not src/a/b/c.ts. No braces, no extglob. The dot parameter is FNM_PERIOD.
  • glob (default) — POSIX plus a ** segment that spans zero or more path segments, plus Bash brace expansion. This is a documented superset matching what Node tooling implements; it is not a published standard, and we say so rather than implying one.
  • extglobglob plus the five Bash operators verbatim from the manual: ?(…) zero or one, *(…) zero or more, +(…) one or more, @(…) exactly one, !(…) anything except one of.
  • gitignore (on /explain, and the whole of /gitignore) — gitignore(5) PATTERN FORMAT with git's three globstar forms. Matching runs over UTF-8 bytes, because git's ? matches one byte: ?.txt does not match é.txt but ??.txt does, and ????.txt matches 😀.txt. Every other dialect matches code points.

What is real here. Every dialect was checked against its primary source and against the real tool on 2026-07-30: git 2.50.1 via git check-ignore in throwaway repositories, BSD fnmatch(3) with FNM_PATHNAME, picomatch 4.0.4, and bash for brace expansion. A committed script re-runs those comparisons against a golden file so a divergence cannot ship undocumented.

Documented divergences — no library parity is claimed.

  • abc/** matches the directory abc itself under glob (and in picomatch) but not in git, where a trailing /** needs at least one segment inside. Both answers are available: ask /test and /gitignore the same question and compare.
  • ? is one code point here, one byte under gitignore, and one UTF-16 code unit in picomatch — three different answers for 😀.txt.
  • [!…] negation. [!a] matches any one character that is not a — POSIX.1-2024 XCU 2.14.1 replaces the regular-expression ^ with ! here, and git and fnmatch(3) both agree. picomatch compiles it to the JavaScript class [!a], where ! is an ordinary member, so it matches a and ! and rejects b — the exact opposite. [^a] works in all four. A ] immediately after the ! is a literal member too, so [!]a] matches b.txt.
  • Braces are a textual expansion, evaluated one expansion at a time, exactly as Bash does: {**,x}/* is **/* OR x/*, and **/* matches the single segment a.ts. picomatch compiles the braces into one alternation and loses the globstar's zero-segment case there, so it answers true for **/* and false for {**,x}/* against the same path. expandedPatterns and matchedPattern show you exactly which expansion won.
  • An unterminated [ has three answers: POSIX.1-2024 XCU 2.14.1 makes a [ that introduces no valid bracket expression match the character itself, so posix/glob/extglob match the literal [; git's matcher makes the rule match nothing, and BSD fnmatch(3) also refuses. A warning tells you which reading applied.
  • A / inside a bracket expression can never match, so a[/]b never matches a/b (git and BSD fnmatch agree). Note that POSIX's own wording identifies slashes before brackets, which would instead make a[b/c]d match only the literal a[b/c]d; both real matchers on hand read the bracket first and so do we.
  • {a,b} in a .gitignore is literal: git has no brace expansion at all. !(a).txt in a .gitignore is a negation of the literal (a).txt, not an extglob group.
  • Brace expansion follows the Bash 5.3 manual, including {1..5..2} and the zero-padding of {01..12}. macOS still ships bash 3.2, which has neither.

Scope limits, stated up front.

  • This is not git check-ignore. One rule file, treated as the repository-root .gitignore. No per-directory .gitignore stacking, no info/exclude, no core.excludesFile, no command-line patterns and no reading of your git config. And a .gitignore never affects a file git already tracks — that is the one cause of "why is my file not ignored" this endpoint cannot see.
  • Slash is always significant. Shell case-style matching where * crosses / (fnmatch() without FNM_PATHNAME) is out of scope.
  • POSIX collating symbols [[.a.]] and equivalence classes [[=a=]] are a 400, not a guess: they are locale-dependent, and git's own matcher makes such a rule match nothing. Write the members out. Character classes are ASCII ([[:alpha:]] is A-Za-z), matching both git and picomatch — a locale-aware fnmatch(3) in a UTF-8 locale would also accept é there, and POSIX leaves that to the locale rather than settling it.
  • . and .. are never resolved. /gitignore rejects them, /test matches them literally with a warning.
  • Case-insensitive mode is an approximation, not a reproduction of APFS, NTFS or core.ignoreCase. Non-NFC input is warned about, because macOS hands out decomposed filenames and a precomposed pattern will not match them.
  • /filter takes one pattern, not a rule file. Testing many paths against a whole .gitignore means calling /gitignore per path: the per-path answer carries ancestorDecisions and competingRules, which do not compress into a list row. Note also that the paths list is comma-separated and each entry is trimmed, so a path containing a comma or a meaningful leading space has to go to /test.
  • Every cap is a documented 400 quoting the real count, and nothing is ever truncated — a truncated rule file or brace expansion gives a wrong boolean, which is worse than an error. Inputs travel in a query string, hence pattern ≤ 512, path ≤ 1024 and rules ≤ 8,000 characters over 500 lines. Newlines travel as %0A, and HTTP query decoding happens before we see the value, so a path that genuinely contains %2F must be sent double-encoded as %252F.
  • There is a published work budget. Matching is a bounded dynamic program — no pattern is ever translated into a regular expression — and a request may spend at most 400,000 cell updates. /test and /gitignore return the steps they actually used, so a*a*a*a*b against forty as is measurably cheap — a few hundred steps — instead of the classic blow-up. Over budget is a 400, never a wrong answer.
  • There is no lookup route and therefore no 404 here — every route computes an answer from what you send.

Use it for

  • Assert in CI that a committed .gitignore still ignores build output and still tracks the files you need
  • Find which .gitignore line ignores a file, and whether a negation was cancelled by an excluded parent directory
  • Check a tsconfig include/exclude or a GitHub Actions paths: filter against real repository paths before pushing
  • Let a coding agent verify an ignore rule it just wrote without being able to run git

Frequently asked questions

Why is my .gitignore not ignoring a file?

Usually one of three things. /gitignore names the first two: reason: "parent-directory" means an ancestor directory is excluded so git never looks inside it, and reason: "no-match" means no rule matches the path at all. The third is outside this endpoint's reach — a .gitignore never affects a file git already tracks, so git rm --cached it first.

Why doesn't !build/keep.txt re-include my file?

Because the line build/ excluded the directory first, and gitignore(5) says a file cannot be re-included when a parent directory of it is excluded. Real git reports the winning rule as line 1 build/, and so does /gitignore, with excludedByParent.directory set to build. Write build/* instead of build/ and the negation works.

Does ** work the same everywhere?

No, and that is why syntax is explicit. posix has no globstar at all — ** is just *. glob and extglob treat a ** segment as zero or more path segments. gitignore has git's three documented forms, and there a trailing /** does not match the directory itself.

Is this the same as minimatch or picomatch?

No, and no bug-for-bug compatibility is claimed. The divergences are listed on this page and re-checked by a committed script: abc/** matches abc here and in picomatch but not in git, ? is a code point here and a UTF-16 code unit in picomatch, and [!]a] matches b.txt here (as in POSIX and git) but not in picomatch.

Do I have to commit the .gitignore first?

No. You paste the file into rules and paste the path into path; nothing is fetched, no filesystem is read and no repository is opened. A draft in your editor tests exactly like a committed one.

Are {a,b} braces supported in .gitignore?

No — git has no brace expansion, so {a,b}.txt matches a file literally named {a,b}.txt and /gitignore warns you about it. Use /expand to see what Bash brace expansion would produce, and /test with syntax=glob to match with braces applied.

Standards & references