Skip to content
GitmoapdocsBeta
Guide· How do I…?

Audit log

Overview

The audit log answers who did what, to which resource, and when — for every change made through Gitmoap, every sign-in, every push and clone, and every read of a credential surface. It is the record you reach for when a repository was deleted and nobody will say by whom, when a compliance questionnaire asks how you evidence administrative access, or when a token has leaked and you need to know exactly what it touched before you revoked it.

It is also tamper-evident. Records are sealed into a hash chain as they are written, and you can replay that chain on demand to prove that nothing in it has been edited, removed or reordered since — including by us. Nothing here can be edited or deleted by anyone, through any interface.

Before you start

  • Owner on the top-level groupto read or verify an organisation's log. This is the group at the top of your tree, the one with no parent; a subgroup is refused even if you own it. Owner is a hard floor with no visibility shortcut: making a group public does not open its audit log to anyone, and a Maintainer cannot read it.
  • Owner on the repositoryto read a single repository's records. Repository access alone is not enough — Developer and Maintainer both get 403 FORBIDDEN here.
  • Platform administratorfor the two /admin calls, which read and verify across every organisation on the deployment. They answer 403 FORBIDDEN for everyone else, including an Owner asking about their own organisation.
  • A token with the right scope, if you are calling the API or the CLI rather than clicking: audit:read to list records, audit:verify to run an integrity check. Scopes are fixed when you mint a token, so a monitoring job that only replays the chain should carry audit:verify alone.
  • Nothing to switch on.Recording is on from the first request; there is no setting, no opt-in and no per-repository toggle. It is equally not retrospective — the log holds what happened after the platform started recording, and nothing before.

How it works

What gets recorded

Every audited request produces exactly one record, written after the request finishes and carrying the status it actually returned. Three kinds of thing are audited, and the category is on every record and on every row of the table.

CategoryWhat it coversExamples
Mutationevery request that changes state — create, update, delete, merge, transfer, member changes, and a git pushdeleting a repository, changing a group's visibility, adding an Owner, merging a merge request, pushing to a branch
Auth eventthe sign-in lifecyclesigning up, signing in, confirming a new account, minting a session
Sensitive readthe short, deliberate list of reads that expose a credential surface — recorded because a read of a secret is the event an exfiltration review asks about firstlisting personal or service-account tokens, reading a provider connection, reading a service account, reading the single sign-on connection, and a git clone or fetch

Ordinary reads are not recorded. Viewing a repository, listing commits, opening a file, reading a merge request — none of these leave a record, and that is deliberate: a log in which the browsing outnumbers the acting is a log nobody reads.

Two things follow from this that surprise people. A refused attempt is still recorded: a 403 on a delete produces a record with an outcome of denied, and a failed request produces one with an outcome of failure, because a rejected attempt is the highest-value line in a security review. And git ref discovery is not recorded — the negotiation that runs at the start of every git command, including the ones that transfer nothing — while the push and the fetch themselves are.

Three chains, and which one you can read

Every record is filed into exactly one chain, and which one follows the target on the record — the most specific thing the request named. This is the single most common source of "why is that not in my log".

ChainWhat lands in itWho reads it
Your organisationrecords whose target is a group or a repository, resolved up to the top-level group — group and repository settings, visibility, transfers and deletions, membership, branch and tag changes, pushes and fetches, and the creation of what lives inside theman Owner of the top-level group, and a platform administrator
A personrecords naming something more specific than a group or a repository — a merge request, a work item, a webhook, a service account, an access token — filed under whoever acted, along with that person's own sign-ins and profile changesa platform administrator
The platformadministrative actions against the deployment itself, and those same specific-target records when the actor was a service account or a pipeline rather than a persona platform administrator

So an organisation's log is the record of what happened to your groups and repositories. It does not hold your people's sign-ins or the personal access tokens they mint on their own accounts, and it does not hold the later edits to an individual merge request or work item — those file under whoever made them. Reaching any of those needs the platform-wide view.

What a record lets you answer

One expanded record carries more than the table shows, and each field is there to close a specific question.

The questionWhere you look on the record
Who did it, and were they a person, a service account or a pipeline?the actor, with their username, email and type
How were they authenticated, and with which token?the authentication method, and the access token's id when a token was used
What did they do, and to what?the action name, the request line, and the target's type and id
Did it work?the outcome, and the HTTP status beside it
When exactly?the occurrence time, to the microsecond
Which request was this in my own logs?the request id, and — on a web or API request — the source address and the user agent
Was it an AI agent acting through the MCP server?the tool the agent called and the application it named itself as — and, when the agent's token came from someone connecting the app by signing in, the app they approved, which is the one to trust where the two differ. One tool call can make several requests; each has its own record, and they share a request id

The actor's name and email are captured as they were at the time and never rewritten, so a record still names the person who acted even after they leave and their account is removed.

Ordering, and the delay you will notice

Records are written just after the request they describe rather than inside it, so a record typically appears within a second of the action and occasionally a little later. If you make a change and immediately reload the log, refresh once more.

Listings come back newest first by when the action happened, and stay in that order across pages, so following the cursor to the end gives you one correctly ordered stream. The sequence number on a record is the chain's own ordering — the order records were sealed — which is close to, but not the same as, the order they occurred. Read it as chain position, not as a timeline. Positions restart at zero at the start of each month, and every record carries the month it sits in, so a sequence number identifies a record only together with that month.

What verification proves

The chain works by sealing each record with a hash over its own contents and the hash of the record before it. Change one character of one record, remove a record, or swap two, and every hash after that point stops matching. Verification replays the whole window and reports the first place the arithmetic fails.

Note

A green result proves that nothing that was recorded has been altered since. It does not prove that everything that happened was recorded. Delivery of a record is best-effort: a crash in the instant between a request finishing and its record being accepted for writing loses that one record silently, and it leaves no gap in the chain to find, because chain positions are assigned when a record is written rather than when the action happened. Treat the chain as evidence of integrity, not as a completeness guarantee, and cross-check against your own request logs where completeness is what you must attest to.

How to

Read your organisation's audit log

The organisation view is the whole estate in one table: every audited action against every group and every repository beneath the top-level group, newest first.

Open the organisation — the top-level group — and choose Audit log in the side navigation, under Manage. The tab appears on the top-level group only; open it on a subgroup and Gitmoap offers you a link across to the organisation's log instead. The table gives you When (an exact timestamp, because this column gets quoted into incident write-ups), Actor, Action, Category, Target and Outcome; click any row to expand the complete record. More rows load as you scroll.

Terminalbash
gitmoap audit list-group-audit-log --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/audit-log \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Narrowing the list is the point of the view. In the web app, Category and Time range are drop-downs, and you set the actor filter by clicking the actor on a row, which then sits beside the bar as a chip you can remove. Over the API and the CLI the same three are --category, --from/--to and --actor, and --resource narrows to a single target id alongside them. --scope is accepted here and changes nothing: an organisation's listing always reads that organisation's own chain.

The time bounds are RFC-3339 instants, so --from 2026-08-01T00:00:00Z. Send them in UTC, the way the records themselves are stamped: a bound is compared against the recorded timestamps as written rather than parsed, so an offset like +02:00 is taken at face value and not converted, and a differently punctuated date such as 2026/08/01 matches nothing at all rather than being refused. The lower bound is inclusive and the upper bound is exclusive, which makes --from 2026-08-01T00:00:00Z --to 2026-09-01T00:00:00Z exactly the month of August with no double-counted midnight.

The actor, category and resource filters are applied to each page after it is read, so a page can come back with fewer rows than you asked for, or with none at all, while still handing you a cursor. That is not the end of the results. Keep following next_cursor until a response comes back without one — that, and only that, is the end. Note that these listings page on cursor and next_cursor, unlike most Gitmoap listings, which use next_token.

Read the audit records for one repository

The repository view is the same table narrowed to one codebase, and it is the right view for handing a single team the record of their own repository without handing them the whole organisation's.

Open the repository, choose Settings, then Audit log. The columns and the row expansion are identical to the organisation view. There is no Verify integrity button here — integrity is a property of the organisation's chain, and it is checked at that level.

Terminalbash
gitmoap audit list-repo-audit-log --repo-id <id>
curlbash
curl -X GET https://api.gitmoap.example/repositories/{repo_id}/audit-log \
-H "Authorization: Bearer $GITMOAP_TOKEN"

A record belongs to the most specific thing the request named, and that one fact decides both which listing shows it and which chain holds it. A repository's log therefore holds the actions whose target is the repository — its settings, visibility, transfer and deletion, its members, its branches and tags, its pushes and fetches, and the creation of something nested inside it, because the repository is the only id such a request carries. Once that thing exists, changing it names the thing itself: editing, merging or deleting a merge request, changing a work item, updating a webhook or a service account all file under the person who acted, so neither this listing nor the organisation's will show them. Following one of those takes a platform administrator reading that person's chain.

This listing walks the repository's whole history rather than the last two years, and its --from and --to bounds are applied to each page after it is read, exactly like --actor and --category. Both mean the same thing in practice: follow the cursor rather than judging by how full a page came back.

Check that the record has not been altered

Verification is a read: it recomputes hashes from what is stored and writes nothing, so you can run it as often as you like, and a scheduled run is a reasonable control to point an auditor at.

Open the organisation's Audit log and choose Verify integrity at the top right. A green banner reports how many records were checked and the sequence range they covered; a red one names the exact sequence position where the chain broke and what went wrong there. Dismiss the banner to return to the table.

Terminalbash
gitmoap audit verify-group-audit-log --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/audit-log/verify \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Left alone, the check covers the current month and the twelve before it. Narrow or widen it with --from-month and --to-month, both YYYY-MM, when you need to attest to a specific period — a quarter, or a year that has since rolled over. Give only --to-month and the window is still thirteen months long, ending there.

A failure names what stopped adding up, and each answer says something different about what happened:

What the result saysWhat it means
a record's hash did not recomputethat record's contents differ from what was sealed — an edit
a record's previous-hash link was brokena record no longer points at the one before it — a reorder, or a substitution
a sequence number was missinga record between two others is gone — a deletion

Any of the three is a serious finding. Record the position it names, the month it falls in and the window you ran, do not clear anything, and raise it — the position is what lets an investigation start at the right record rather than at the beginning. Because the replay runs a month at a time and positions restart each month, narrow the window to a single month to pin down which one broke.

Read across every organisation

The two calls below belong to Gitmoap's platform administrators, not to an organisation's Owner. They need a token carrying audit:read or audit:verify and platform administrator rights, and they answer 403 FORBIDDEN otherwise.

Without a --scope, the platform-wide listing reads the platform's own chain. Give it --scope to read one organisation's or one person's chain — the value names the chain rather than the resource, so it is ROOT# followed by the top-level group's id, or USER# followed by the person's id — the same value that appears on every record it returns. This is how you reach the sign-in history of a single account, which no organisation-level view exposes. Give it --actor or --resource instead and it searches across every chain for one person or one target, which is the query to run when you know a token has leaked and need everything it touched, wherever it happened. Those three choose the listing in that order — a --scope wins over an --actor, and an --actor over a --resource — and whichever you did not choose is then applied to each page as an ordinary filter.

Gitmoap hosts no platform-wide audit screen. Read it through the API or the CLI with an administrator token.

Terminalbash
gitmoap audit list-admin-audit-log
curlbash
curl -X GET https://api.gitmoap.example/admin/audit-log \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Verify any chain as an administrator

The administrator's verification takes the same month window as the organisation's and additionally lets you name which chain to replay, so a person's chain and the platform's own can be attested to as well.

Gitmoap hosts no platform-wide verification screen. Run it through the API or the CLI with an administrator token.

Terminalbash
gitmoap audit verify-admin-audit-log
curlbash
curl -X GET https://api.gitmoap.example/admin/audit-log/verify \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Left alone it verifies the platform's own chain over the current month and the twelve before it.

Reference

5 endpoints

GET/groups/{group_id}/audit-logaudit:read

List an organization's audit log

list_group_audit_logGET /groups/{group_id}/audit-logOpen in the API reference

Token scope

audit:read

Send

Path
  • group_iduuidThe organization's top-level group
Query
  • actorstringoptionalFilter to one actor id
  • resourcestringoptionalFilter to one target resource id
  • categorystringoptionalFilter to one category (mutation/auth_event/sensitive_read)
  • fromstringoptionalLower time bound (RFC-3339, inclusive)
  • tostringoptionalUpper time bound (RFC-3339, exclusive)
  • limitint32optionalPage size (1-100, default 20)
  • cursorstringoptionalOpaque pagination cursor
  • scopestringoptionalIgnored here: this listing always reads the organization's own audit trail

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Not an Owner of the group
  • 404Group not found
  • 501The audit log is not configured on this deployment

Receive

  • next_cursorobjectoptional
  • records[]object23 fields
    • accessobjectoptional
    • actorobject7 fields
      • actor_typestringWhat kind of caller acted: user, service_account, pipeline_run, anonymous, or scim — an organization's identity provider calling with its SCIM token.
      • auth_methodaccess_tokenanonymousjwtHow the caller authenticated.
      • avatar_urlobjectoptional
      • emailstringoptional
      • idstringThe id of the user, service account or pipeline run that acted, or anonymous.
      • token_idobjectoptional
      • usernamestringoptional
    • audit_iduuid
    • categoryauth_eventmutationsensitive_readWhat kind of operation the record captures.
    • hashstringSHA256(canonical_json(record without hash) ‖ prev_hash), hex.
    • http_methodstring
    • mcpobjectoptional
    • monthstringThe month the record belongs to, YYYY-MM. Each month's records form one segment of the chain.
    • occurred_atstring
    • operation_idstring
    • outcomedeniedfailuresuccessThe result of the audited operation.
    • prev_hashstringHash of the previous record in the chain (genesis = 64 zeros; first of a month links to the prior month's tip).
    • recorded_atstringWhen the record was written to the log, RFC 3339 with microseconds.
    • request_idstring
    • resource_bindingobjectoptional
    • routestring
    • scopestringWhich audit chain the record belongs to — an organisation, a user, or the platform.
GET/repositories/{repo_id}/audit-logaudit:read

List the audit records targeting a repository

list_repo_audit_logGET /repositories/{repo_id}/audit-logOpen in the API reference

Token scope

audit:read

Send

Path
  • repo_iduuidRepository id
Query
  • actorstringoptionalFilter to one actor id
  • categorystringoptionalFilter to one category
  • fromstringoptionalLower time bound (RFC-3339, inclusive)
  • tostringoptionalUpper time bound (RFC-3339, exclusive)
  • limitint32optionalPage size (1-100, default 20)
  • cursorstringoptionalOpaque pagination cursor
  • resourcestringoptionalFilter to one target resource id
  • scopestringoptionalIgnored here: this listing always reads the records targeting the repository

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Not an Owner of the repository
  • 404Repository not found
  • 501The audit log is not configured on this deployment

Receive

The same shape list_group_audit_log returns — 2 fields. Go to it

Show it here
  • next_cursorobjectoptional
  • records[]object23 fields
    • accessobjectoptional
    • actorobject7 fields
      • actor_typestringWhat kind of caller acted: user, service_account, pipeline_run, anonymous, or scim — an organization's identity provider calling with its SCIM token.
      • auth_methodaccess_tokenanonymousjwtHow the caller authenticated.
      • avatar_urlobjectoptional
      • emailstringoptional
      • idstringThe id of the user, service account or pipeline run that acted, or anonymous.
      • token_idobjectoptional
      • usernamestringoptional
    • audit_iduuid
    • categoryauth_eventmutationsensitive_readWhat kind of operation the record captures.
    • hashstringSHA256(canonical_json(record without hash) ‖ prev_hash), hex.
    • http_methodstring
    • mcpobjectoptional
    • monthstringThe month the record belongs to, YYYY-MM. Each month's records form one segment of the chain.
    • occurred_atstring
    • operation_idstring
    • outcomedeniedfailuresuccessThe result of the audited operation.
    • prev_hashstringHash of the previous record in the chain (genesis = 64 zeros; first of a month links to the prior month's tip).
    • recorded_atstringWhen the record was written to the log, RFC 3339 with microseconds.
    • request_idstring
    • resource_bindingobjectoptional
    • routestring
    • scopestringWhich audit chain the record belongs to — an organisation, a user, or the platform.
GET/groups/{group_id}/audit-log/verifyaudit:verify

Verify an organization's audit-chain integrity

verify_group_audit_logGET /groups/{group_id}/audit-log/verifyOpen in the API reference

Token scope

audit:verify

Send

Path
  • group_iduuidThe organization's top-level group
Query
  • from_monthstringoptionalFirst month to check (YYYY-MM; default 12 months before to_month)
  • to_monthstringoptionalLast month to check (YYYY-MM; default the current month)

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Not an Owner of the group
  • 404Group not found
  • 501The audit log is not configured on this deployment

Receive

  • broken_at_seqint64optional
  • checked_countint64
  • failure_reasonobjectoptional
  • first_seqint64optional
  • last_seqint64optional
  • scopestring
  • verifiedboolean
GET/admin/audit-logaudit:read

Platform-wide audit log

list_admin_audit_logGET /admin/audit-logOpen in the API reference

Token scope

audit:read

Send

Query
  • scopestringoptionalThe audit trail to read: PLATFORM, ROOT# followed by an organization's top-level group id, or USER# followed by a user id
  • actorstringoptionalFilter to one actor id
  • resourcestringoptionalFilter to one target resource id
  • categorystringoptionalFilter to one category
  • fromstringoptionalLower time bound (RFC-3339, inclusive)
  • tostringoptionalUpper time bound (RFC-3339, exclusive)
  • limitint32optionalPage size (1-100, default 20)
  • cursorstringoptionalOpaque pagination cursor

Refuses

  • 400A query parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Not a platform administrator
  • 501The audit log is not configured on this deployment

Receive

The same shape list_group_audit_log returns — 2 fields. Go to it

Show it here
  • next_cursorobjectoptional
  • records[]object23 fields
    • accessobjectoptional
    • actorobject7 fields
      • actor_typestringWhat kind of caller acted: user, service_account, pipeline_run, anonymous, or scim — an organization's identity provider calling with its SCIM token.
      • auth_methodaccess_tokenanonymousjwtHow the caller authenticated.
      • avatar_urlobjectoptional
      • emailstringoptional
      • idstringThe id of the user, service account or pipeline run that acted, or anonymous.
      • token_idobjectoptional
      • usernamestringoptional
    • audit_iduuid
    • categoryauth_eventmutationsensitive_readWhat kind of operation the record captures.
    • hashstringSHA256(canonical_json(record without hash) ‖ prev_hash), hex.
    • http_methodstring
    • mcpobjectoptional
    • monthstringThe month the record belongs to, YYYY-MM. Each month's records form one segment of the chain.
    • occurred_atstring
    • operation_idstring
    • outcomedeniedfailuresuccessThe result of the audited operation.
    • prev_hashstringHash of the previous record in the chain (genesis = 64 zeros; first of a month links to the prior month's tip).
    • recorded_atstringWhen the record was written to the log, RFC 3339 with microseconds.
    • request_idstring
    • resource_bindingobjectoptional
    • routestring
    • scopestringWhich audit chain the record belongs to — an organisation, a user, or the platform.
GET/admin/audit-log/verifyaudit:verify

Verify any audit trail's chain integrity

verify_admin_audit_logGET /admin/audit-log/verifyOpen in the API reference

Token scope

audit:verify

Send

Query
  • scopestringoptionalThe audit trail to verify: PLATFORM (the default), ROOT# followed by an organization's top-level group id, or USER# followed by a user id
  • from_monthstringoptionalFirst month to check (YYYY-MM; default 12 months before to_month)
  • to_monthstringoptionalLast month to check (YYYY-MM; default the current month)

Refuses

  • 401Unauthenticated — no credential, or an invalid one
  • 403Not a platform administrator
  • 501The audit log is not configured on this deployment

Receive

The same shape verify_group_audit_log returns — 7 fields. Go to it

Show it here
  • broken_at_seqint64optional
  • checked_countint64
  • failure_reasonobjectoptional
  • first_seqint64optional
  • last_seqint64optional
  • scopestring
  • verifiedboolean

Limits

Records are kept indefinitely. Nothing expires them, nothing prunes them, and no interface deletes one — expiry would leave holes in the chain, which is the one thing the chain exists to make impossible. Deleting a repository, a group or a person does not remove the records about them either; outliving what they describe is the whole point of an audit trail.

A page holds at most 100 records, and 20 if you do not ask. A larger limit is clamped to 100 rather than refused, so a request for 500 returns 100 and a cursor, not an error. Reading a longer period means following the cursor.

An organisation listing reaches back 24 months from the newest month it starts at. That is the default floor, and it applies when you set no lower bound — the web app's All time range sends none, so it stops there too. To read further back than two years, give an explicit --from, and the listing walks back to that month instead. The floor is a property of reading a chain: a repository's listing is served by target rather than by month and has no such limit, and neither does a platform-wide search by --actor or --resource.

Verification defaults to the current month and the twelve before it, and covers exactly the months you name when you set --from-month and --to-month. Each month in the window is replayed on its own, so read the record count in the result alongside the verdict: a window that names no months in which anything happened returns a pass over zero records, which is not the same as a pass.

There is no quota on reading the audit log and no charge for verifying it.

Troubleshooting

400 BAD_REQUESTmentioning the root namespace, on a group you own
Cause
The id names a subgroup. An audit chain belongs to the organisation, not to each group in it
What to do
Pass the id of the top-level group — the one with no parent. In the web app, open that group's Audit log; a subgroup's own audit address offers a link across to it
400 BAD_REQUESTon any call, mentioning an id
Cause
A path segment is not a well-formed id — usually a path like my-group/my-repo pasted where an id belongs
What to do
Use the id. The CLI accepts a path for --group-id and --repo-id and resolves it for you
400 BAD_REQUESTnaming the category
Cause
The category value is not one of mutation, auth_event, sensitive_read
What to do
Use one of the three exactly as spelled — lower case with an underscore. Mutation and auth-event are both refused
400 BAD_REQUESTmentioning the page size
Cause
limit is not a whole number
What to do
Send a number. A number outside 1–100 is not an error: it is clamped to the nearest end
400 BAD_REQUESTmentioning the cursor
Cause
The cursor was truncated or re-encoded in transit, or it came from a different listing
What to do
Drop it and start again from the first page. Pass a cursor back only to the listing that produced it — an organisation cursor and a repository cursor are not interchangeable
401 UNAUTHORIZED
Cause
No credential was sent, or the token has expired or been revoked
What to do
Sign in again, or mint a new token. The audit log has no anonymous access at any visibility, so a public group answers 401 here just as a private one does
403 FORBIDDENon a group you are a member of
Cause
Your access level on the top-level group is below Owner
What to do
Ask an Owner to raise your level there. There is no read-only tier for the audit log and no visibility setting that opens it
403 FORBIDDENon a repository you administer
Cause
Reading a repository's records needs Owner on that repository; Maintainer is not enough
What to do
Ask an Owner of the repository, or read the same records from the organisation's log if you are an Owner there
403 FORBIDDENon /admin/audit-log
Cause
The call is for Gitmoap's platform administrators, and owning an organisation grants nothing on it
What to do
Use your organisation's own audit log. If you need a person's sign-in history or a search across organisations, ask a platform administrator to run it
403 INSUFFICIENT_SCOPE
Cause
The token is valid but was minted without the scope this call needs
What to do
Mint a new token carrying audit:read for the listings or audit:verify for the integrity check. Scopes are fixed at mint time and cannot be added afterwards
404 NOT_FOUNDon a group or repository you are sure exists
Cause
The resource is private and you are not a member, or the id is wrong
What to do
A private resource answers 404 rather than 403 so that its existence stays private. Confirm your membership, then confirm the id
501 NOT_IMPLEMENTEDsaying the audit log is not configured
Cause
The audit log is unavailable on Gitmoap's side, so there is nothing to read
What to do
Nothing you can do from the client — contact support@gitmoap.com. Every other call keeps working: the records are written and the chain is kept, and only the reads are unavailable
An empty page, with a cursor still returned
Cause
The actor, category and resource filters — and, on a repository listing, the time bounds — are applied to each page after it is read, so a page can be emptied by them while more results remain behind it
What to do
Keep following next_cursor. The listing is finished only when a response comes back without one
A time-filtered listing returns nothing, or the window is off by some hours
Cause
Bounds are compared against the recorded timestamps as written rather than parsed, so a shape that does not sort like them matches nothing, and an offset such as +02:00 is read literally instead of being converted
What to do
Send a UTC instant: 2026-08-01T00:00:00Z. Convert a local time yourself before sending it, and never punctuate the date any other way
A listing stops at records about two years old
Cause
With no lower bound, an organisation listing walks back 24 months and stops
What to do
Set --from to the month you want to reach and run it again
An action you performed is not in the log
Cause
It was an ordinary read — reads are not audited unless they expose a credential surface — or it was a ref discovery rather than a push or fetch
What to do
Check the category the action would fall in. If it changed something and is genuinely missing, note the exact time and raise it: a lost record is possible and is worth knowing about
A merge request or work item change is in neither the repository's log nor the organisation's
Cause
The record names that merge request or work item as its target, so it files under the person who made the change
What to do
Ask a platform administrator to read that person's chain, or to search every chain by actor. Creations, settings, membership, branches, pushes and fetches all stay in the organisation's log
Sign-ins are missing from the organisation's log
Cause
Sign-in events belong to the person's own chain, not to any organisation's
What to do
Ask a platform administrator to read that person's chain with --scope
A verification passes with zero records checked
Cause
The month window holds nothing — either genuinely, or because the first month is later than the last
What to do
Check the order of --from-month and --to-month, then widen the window. A pass over zero records attests to nothing
A verification fails
Cause
A record in that window was altered, removed or reordered
What to do
Note the position, the window you ran and the month it lands in, leave everything as it is, and raise it. Re-running one month at a time narrows the break to a single month, which is where an investigation starts
The same action appears twice, moments apart
Cause
Record delivery is at-least-once, and a rare retry can write a second copy
What to do
Deduplicate on the record's own id. Duplicates are harmless and do not affect verification, which checks the chain as written
An actor shows as unknown with no email
Cause
The account could not be resolved at the moment the record was written
What to do
The actor's id and type on the record are still exact. Look the id up directly to identify them
A record appears seconds after the action, not instantly
Cause
Records are written just after the request rather than inside it
What to do
Reload once. If a record has not appeared after a few seconds, it is worth raising rather than waiting
Sequence numbers do not run in the order the rows are displayed, or drop back to zero part-way down
Cause
The list is ordered by when actions happened; the sequence number is the chain's own write order, and it restarts at zero at the start of each month
What to do
Read the timestamp for chronology and the sequence number, together with the record's month, for chain position. Neither is wrong

Examples

Pull a full month for one organisation

The listing pages, so an export is a loop over the cursor. This reads August in pages of 100 and writes one record per line:

shellscript
GROUP=acme
CURSOR=""
: > august.jsonl
while :; do
PAGE=$(gitmoap audit list-group-audit-log \
--group-id "$GROUP" \
--from 2026-08-01T00:00:00Z \
--to 2026-09-01T00:00:00Z \
--limit 100 \
${CURSOR:+--cursor "$CURSOR"} \
--output json)
echo "$PAGE" | jq -c '.records[]' >> august.jsonl
CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')
[ -n "$CURSOR" ] || break
done
wc -l august.jsonl

Replace acme with your own top-level group; the CLI accepts either its path or its id. The upper bound is exclusive, so the window is August and nothing of September.

Find out what a leaked token did

Revoke the token first — that stops the bleeding, and the audit log will still name it, because the token's id is on every record it produced. Then read the organisation's log for the window between the token being minted and being revoked, and look for the access token id on the expanded records.

shellscript
gitmoap audit list-group-audit-log \
--group-id acme \
--from 2026-08-14T00:00:00Z \
--limit 100 \
--output json | jq '.records[] | select(.actor.token_id == "8f14e45f-e2a1-4b2e-9d1f-0c3a7b6d5e42")'

Anything the token did that named something more specific than a group or a repository — a merge request, a work item, a webhook — sits on the actor's own chain rather than the organisation's. Ask a platform administrator to search by actor across every chain, which is the one query that reaches all three.

Attest to a closed quarter

shellscript
gitmoap audit verify-group-audit-log --group-id acme --from-month 2026-04 --to-month 2026-06

Keep the result: it names the verdict, how many records were checked and the chain positions the replay covered, which is what an auditor asks for. Check that count against what you expect of the quarter — a pass over far fewer records than the quarter's activity would produce is telling you the window was wrong, not that the quarter was quiet. Run it again whenever you need the attestation refreshed; verification reads and never writes, so it can be run any number of times over the same period.

FAQ

Can anyone delete or edit a record?

No — not an Owner, not a platform administrator, not us. There is no interface that mutates or removes a record, and the storage the records live in is granted append-only rights, so even a defective writer cannot overwrite one. The hash chain is the check on top of that: it makes any change that did happen visible.

How do I export the log?

Follow the cursor, as in the example above. Every field the API returns is on the record, so a line-per-record file is a complete extract of the window you asked for.

Does it record who read things?

Only reads that expose a credential surface — the token listings, provider connections, service accounts, the single sign-on connection, and git clones and fetches. Ordinary browsing is not recorded.

Why does my subgroup have no audit log?

A chain belongs to the organisation, so a subgroup's records are in the top-level group's log, filed there from the moment they were written. Nothing is missing; the reading happens one level up.

Do service accounts and pipelines show up?

Yes. Each record names the actor's type, so a service account, a pipeline run and a person are distinguishable at a glance, and the actor filter works the same way for all of them.

Can I get an alert when the chain breaks?

Run the verification on a schedule with a token carrying audit:verify and alert on a result that is not verified. It writes nothing, so running it hourly costs you nothing but the request.

How far back does it go?

As far as your organisation does. Records are never expired — the only ceiling you meet is an organisation listing's default reach of 24 months, which an explicit --from moves.

  • Access tokensminting a token that carries audit:read or audit:verify, and revoking one whose activity you have just traced
  • Groupsthe top-level group an audit chain belongs to, and the Owner access that reading it needs
  • Service accountsthe non-human identities that appear as actors, and the reads of them that are themselves recorded
  • Single sign-onconnecting an identity provider, and where a person's sign-in history is filed
  • Repositoriesthe settings, transfers and deletions that make up most of what a repository's log records
  • Webhookswhen you want to be told about an event as it happens, rather than to look it up afterwards