Contents
Audit trail
gitta records who started each agent, what it changed, what was checked, what was stopped and what was merged. Every record links to the one before it, so a change to the record is found.
Why an audit trail for AI coding agents
When agents write code, an auditor asks new questions. Which changes came from an agent? Who started it? Was the change tested with the other work merged at the same time? Were tests removed or checks skipped? Git history shows commits and authors. The gitta trail adds the agent, the checks and the decisions around each change, in one record that you can verify and export.
What is recorded
| Event | When | Content |
|---|---|---|
agent_started | A new agent worktree appears | Tool, branch, starting commit, path |
warning | An agent is told about another agent's change, or about a security note on its own new code | Kind, symbol, file, the other agent and its machine. For a security note: the rule, severity, file and line |
security_allowed | An allow marker or an [allow] entry covers a security note | The rule, file, line and reason, and whether the agent's own change added the allow |
merge_checked | A git merge, git pull or git push to the protected branch passes the checks | The target branch, the incoming commit, the git command, the files and the test command with its result and time |
merge_stopped | gitta stops a merge, pull or push because the tests fail on the combined code | The target branch, the incoming commit, the git command and the failing tests |
checks_skipped | A merge ran with --no-verify, or a command moved the protected branch with GITTA_SKIP | The target branch, the merge commit and the command |
warned | A merge removes tests or weakens a check, and weakened_checks is warn | The findings, the setting and who ran the merge |
blocked | The same, and weakened_checks is block, so the merge stops | The findings, the setting and who ran the merge |
checks | Before gitta land moves main | Each check and its result: weakened checks, security, security rules with each note and the threshold, credentials, changed meaning, merge and tests. Also the test command and its time |
land_refused | A landing is stopped | The check that stopped it, and why |
landed | gitta land moves main | Old and new commit, exact file versions, the agent's commits with authors and trailers such as Co-Authored-By, approvals |
reached_main | Work arrives by another route, for example a merged pull request, a push or git merge | The commits, authors, committers, trailers, and how many name an AI co-author |
approved | A person records an optional override with gitta approve | Who, the reason, the exact findings |
Each event also records the person (Git name and email, operating system user) and the machine. No source code is recorded. gitta records paths, symbols, commits and results only.
The trail is on from gitta init. It is stored in .gitta/audit.jsonl, one event per line.
Read it
$ gitta audit
2026-10-22T14:43:57Z agent_started ci-level dev@workstation git worktree workspace on branch ci-level from e1091763d4
2026-10-22T14:43:57Z agent_started ci99 dev@workstation git worktree workspace on branch ci99 from e1091763d4
2026-10-22T14:43:58Z warning ci99 dev@workstation HAZARD bench.aidev-replay.analyze.wilson in bench/aidev-replay/analyze.py (with ci-level)
2026-10-22T14:43:59Z merge_checked ci-level dev@workstation
2026-10-22T14:44:00Z merge_stopped ci99 dev@workstation
2026-10-22T14:44:00Z reached_main dev@workstation
2026-10-22T14:44:01Z merge_checked ci99 dev@workstation
...Filter by date, agent or event. --json shows every field of each event:
$ gitta audit --since 2026-10-01 --event merge_stopped
$ gitta audit --agent ci99 --jsonCheck that nobody changed it
Each event carries the SHA-256 hash of the event before it. If someone changes, removes or reorders an event, the chain breaks at that point.
$ gitta audit verify
gitta: audit trail intact: 39 event(s), every one linked to the one before
$ gitta audit verify # after someone edited .gitta/audit.jsonl
gitta: audit trail BROKEN: event 29 (merge_checked, 2026-10-21T09:50:39Z) was changed after it was writtenFor a record that nobody on a machine can rewrite, use the team server. Each machine sends its events to the servers as they happen, and the servers keep a copy that the machine cannot change. gitta audit verify --team checks that copy.
Export it
$ gitta audit --csv > evidence.csv
$ gitta audit --json > events.jsonltime,event,agent,person,email,machine,summary,hash
"2026-10-22T09:47:05Z","agent_started","worktree-detect-language","dev","[email protected]","workstation","Claude Code workspace on branch worktree-detect-language from a975ef6486","ff84c7a4670f0c4281377eb3435ffbdfb33c4a909ae25adb057952961cbfa2e6"
"2026-10-22T09:47:05Z","agent_started","worktree-translate-api","dev","[email protected]","workstation","Claude Code workspace on branch worktree-translate-api from a975ef6486","193096e09832a4f8ead6226bee2d721f4eacaac6613eed156076ddb0cf54de71"Use CSV for spreadsheets and auditors. Use JSON lines for a log system or SIEM.
Send events to your log system
With gitta Enterprise, the team server sends each audit event to your log system the moment it stores it. Turn on one or more outputs with a setting on the server. Each event keeps its seq, prev and hash fields, so you can still check the chain in your log system. The server adds repo and severity, and gitta audit verify leaves those two out of the hash.
Each output has its own queue on disk, in the server's data folder. When your log system is slow or down, the server keeps the events in that queue and sends them later, in order, also after a restart. It tries again after 1 second, then waits twice as long each time, up to 1 minute. Merges, landings and agents never wait for a log system. The server also keeps its own copy of every event, so gitta audit --team works while a log system is down.
| Setting | Use |
|---|---|
GITTA_LOG_STDOUT=1 or --log-stdout | One JSON line per event on standard output. Docker, Kubernetes, Google Cloud Logging, Amazon CloudWatch and Azure Monitor collect it with no extra agent. |
GITTA_SYSLOG or --syslog URL | Syslog in RFC 5424 format. Use udp://host:514, tcp://host:601 or tls://host:6514. |
GITTA_SYSLOG_CA or --syslog-ca FILE | A PEM file with your own certificate authority, for a syslog server with a private certificate. |
GITTA_LOG_WEBHOOK or --log-webhook URL | An HTTPS address that takes events in batches of up to 100. |
GITTA_LOG_WEBHOOK_FORMAT | json (the default, a JSON array of events), splunk for the Splunk HTTP Event Collector, or datadog. |
GITTA_LOG_WEBHOOK_TOKEN or --log-webhook-token-file FILE | The token for the receiver. gitta sends it as Authorization: Splunk, DD-API-KEY or Authorization: Bearer, to match the format. |
GITTA_LOG_WEBHOOK_HEADER | One more header for your own endpoint, written as Name: value. |
GITTA_LOG_WEBHOOK_CA | A PEM file with your own certificate authority, for an endpoint with a private certificate. |
GITTA_LOG_QUEUE_MB | The size of each queue on disk. The default is 256 MB. |
Syslog
Each event is one message with facility 13 (log audit). The event name is the message ID, the chain fields are structured data, and the full event is JSON in the message body. Blocked merges and refused landings have severity warning. Over TCP and TLS, each message is framed with its length (RFC 6587), which rsyslog, syslog‑ng and most SIEM collectors read as is.
$ export GITTA_SYSLOG=tls://syslog.example.com:6514
$ export GITTA_SYSLOG_CA=/etc/gitta/company-ca.pem
$ gitta server --data /data<108>1 2026-10-22T09:47:05Z workstation gitta - land_refused [gitta@32473 seq="12" prev="9c1e…" hash="4b7a…" source="5f0c…" repo="github.com/acme/shop" agent="leaky" person="Dev" email="[email protected]"] {"event":"land_refused",…}Google Cloud Logging
Cloud Run and GKE send standard output to Cloud Logging. Cloud Logging reads the severity field of each line, so blocked merges show as warnings.
$ gcloud run services update gitta --region europe-north1 --update-env-vars GITTA_LOG_STDOUT=1Find the events in the Logs Explorer with this query.
resource.type="cloud_run_revision"
jsonPayload.event="landed"Amazon CloudWatch
On ECS or Fargate, give the container the awslogs log driver and set GITTA_LOG_STDOUT. CloudWatch Logs Insights reads the JSON fields.
"environment": [{"name": "GITTA_LOG_STDOUT", "value": "1"}],
"logConfiguration": {
"logDriver": "awslogs",
"options": {"awslogs-group": "/gitta/audit", "awslogs-region": "eu-north-1", "awslogs-stream-prefix": "gitta"}
}fields time, event, agent, actor.name, repo
| filter severity = "WARNING"
| sort time descAzure Monitor
Azure Container Apps sends standard output to your Log Analytics workspace.
$ az containerapp update --name gitta --resource-group gitta --set-env-vars GITTA_LOG_STDOUT=1ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "gitta"
| extend e = parse_json(Log_s)
| where e.event == "blocked"Kubernetes and Docker
Set GITTA_LOG_STDOUT=1 in the container. The log collector of your cluster, such as Fluent Bit or the agent of your cloud, then sends each line on.
$ docker run -d --name gitta -e GITTA_LOG_STDOUT=1 -e GITTA_TOKENS -v gitta-data:/data -p 8080:8080 europe-north1-docker.pkg.dev/gitta-io-prod/enterprise/gitta-server:0.1.0
$ docker logs -f gittaSplunk
Create an HTTP Event Collector token in Splunk, then point the server at the collector. Each event arrives with source gitta and source type _json.
$ export GITTA_LOG_WEBHOOK=https://splunk.example.com:8088/services/collector/event
$ export GITTA_LOG_WEBHOOK_FORMAT=splunk
$ export GITTA_LOG_WEBHOOK_TOKEN="$SPLUNK_HEC_TOKEN"
$ gitta server --data /dataDatadog
Use the logs intake address of your Datadog site and an API key. Events arrive with source gitta and a status that follows the severity.
$ export GITTA_LOG_WEBHOOK=https://http-intake.logs.datadoghq.com/api/v2/logs
$ export GITTA_LOG_WEBHOOK_FORMAT=datadog
$ export GITTA_LOG_WEBHOOK_TOKEN="$DD_API_KEY"
$ gitta server --data /dataYour own endpoint
With the json format, the server sends a POST with a JSON array of events. Any 2xx answer counts as received. Any other answer, or no answer, makes the server try again later.
$ export GITTA_LOG_WEBHOOK=https://logs.example.com/gitta
$ export GITTA_LOG_WEBHOOK_HEADER="X-Team: platform"
$ export GITTA_LOG_WEBHOOK_TOKEN="$LOGS_TOKEN"From one laptop
Without a team server, gitta audit --follow prints each new event as gitta writes it. Add --json to get one JSON line per event and send it on with any tool. --agent and --event filter the stream, and --since prints the earlier events first.
$ gitta audit --follow --json >> ~/gitta-audit.jsonl
$ gitta audit --follow --json | logger --tag gitta
$ gitta audit --follow --event landedNotes on landed commits
Each commit that gitta land makes gets a Git note with the agent, the tool, who landed it and the checks. Notes are a standard Git feature and do not change the commit.
$ git notes --ref=gitta show main | head -14
{
"gitta": 1,
"agent": "quickfix",
"tool": "git worktree",
"landed_by": {
"name": "dev",
"email": "[email protected]",
"os_user": "dev",
"machine": "workstation"
},
"checks": [
{
"check": "weakened checks",
"result": "none found"
...To share notes with your team, push them with git push origin refs/notes/gitta.
SOC 2 and ISO 27001
gitta gives evidence for the controls that auditors ask about when AI agents write code. The table maps each question to the events that answer it.
| Question an auditor asks | Evidence | SOC 2 | ISO/IEC 27001:2022 |
|---|---|---|---|
| Who made this change, and was an AI agent involved? | agent_started, landed, reached_main | CC8.1 | 8.32, 8.4 |
| Was the change tested before it reached production code? | merge_checked, merge_stopped, checks | CC8.1 | 8.29, 8.25 |
| Were checks weakened or skipped, and who merged it? | warned, blocked, checks_skipped, approved | CC8.1, CC6.8 | 8.32, 8.28 |
| Were security controls weakened, or risky code added, and who merged it? | warning, checks, approved, security_allowed | CC8.1, CC6.8 | 8.28, 8.32 |
| Can secrets end up in the code? | warned and blocked for credentials | CC6.1 | 8.28 |
| Are conflicts between parallel changes found? | warning | CC7.2 | 8.16 |
| Can the record be trusted? | Hash chain, gitta audit verify, server copies | CC7.2 | 8.15, 5.33 |
To map these records to SOC 2 and ISO 27001 controls in more detail, see Compliance. For what gitta checks in each change, see Security and data.