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 FORBIDDENhere. - Platform administratorfor the two
/admincalls, which read and verify across every organisation on the deployment. They answer403 FORBIDDENfor 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:readto list records,audit:verifyto run an integrity check. Scopes are fixed when you mint a token, so a monitoring job that only replays the chain should carryaudit:verifyalone. - 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.
| Category | What it covers | Examples |
|---|---|---|
| Mutation | every request that changes state — create, update, delete, merge, transfer, member changes, and a git push | deleting a repository, changing a group's visibility, adding an Owner, merging a merge request, pushing to a branch |
| Auth event | the sign-in lifecycle | signing up, signing in, confirming a new account, minting a session |
| Sensitive read | the 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 first | listing 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".
| Chain | What lands in it | Who reads it |
|---|---|---|
| Your organisation | records 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 them | an Owner of the top-level group, and a platform administrator |
| A person | records 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 changes | a platform administrator |
| The platform | administrative actions against the deployment itself, and those same specific-target records when the actor was a service account or a pipeline rather than a person | a 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 question | Where 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.
gitmoap audit list-group-audit-log --group-id <id>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.
gitmoap audit list-repo-audit-log --repo-id <id>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.
gitmoap audit verify-group-audit-log --group-id <id>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 says | What it means |
|---|---|
| a record's hash did not recompute | that record's contents differ from what was sealed — an edit |
| a record's previous-hash link was broken | a record no longer points at the one before it — a reorder, or a substitution |
| a sequence number was missing | a 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.
gitmoap audit list-admin-audit-logcurl -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.
gitmoap audit verify-admin-audit-logcurl -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
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 type401Unauthenticated — no credential, or an invalid one403Not an Owner of the group404Group not found501The 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, orscim— 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
- actor_typestringWhat kind of caller acted:
- audit_iduuid
- categoryauth_eventmutationsensitive_readWhat kind of operation the record captures.
- hashstring
SHA256(canonical_json(record withouthash) ‖ 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
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 type401Unauthenticated — no credential, or an invalid one403Not an Owner of the repository404Repository not found501The 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, orscim— 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
- actor_typestringWhat kind of caller acted:
- audit_iduuid
- categoryauth_eventmutationsensitive_readWhat kind of operation the record captures.
- hashstring
SHA256(canonical_json(record withouthash) ‖ 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
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 type401Unauthenticated — no credential, or an invalid one403Not an Owner of the group404Group not found501The 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
Token scope
audit:read
Send
Query
- scopestringoptionalThe audit trail to read:
PLATFORM,ROOT#followed by an organization's top-level group id, orUSER#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 type401Unauthenticated — no credential, or an invalid one403Not a platform administrator501The 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, orscim— 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
- actor_typestringWhat kind of caller acted:
- audit_iduuid
- categoryauth_eventmutationsensitive_readWhat kind of operation the record captures.
- hashstring
SHA256(canonical_json(record withouthash) ‖ 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
Token scope
audit:verify
Send
Query
- scopestringoptionalThe audit trail to verify:
PLATFORM(the default),ROOT#followed by an organization's top-level group id, orUSER#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 one403Not a platform administrator501The 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-repopasted where an id belongs - What to do
- Use the id. The CLI accepts a path for
--group-idand--repo-idand resolves it for you
400 BAD_REQUESTnaming the category
- Cause
- The
categoryvalue is not one ofmutation,auth_event,sensitive_read - What to do
- Use one of the three exactly as spelled — lower case with an underscore.
Mutationandauth-eventare both refused
400 BAD_REQUESTmentioning the page size
- Cause
limitis 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
401here 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:readfor the listings oraudit:verifyfor 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
404rather than403so 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:00is 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
--fromto 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-monthand--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:
GROUP=acmeCURSOR="": > august.jsonlwhile :; 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" ] || breakdonewc -l august.jsonlReplace 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.
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
gitmoap audit verify-group-audit-log --group-id acme --from-month 2026-04 --to-month 2026-06Keep 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.
Related
- Access tokensminting a token that carries
audit:readoraudit: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