What an ISO 42001 auditor may ask to see if you only use Claude
The short answer
Our reading: an auditor of an ISO/IEC 42001 management system wants an inventory of the AI systems and tools you use, who owns each, and records showing the list is kept current. This guide builds that inventory from what Claude’s Activity Feed shows was added, then compares it with your own AI register.
What the standard asks of a company that only uses AI
ISO/IEC 42001:2023 is a standard for an AI management system. It is written for organizations that build, provide or use AI, so a company that only uses Claude has its own part of it. The standard’s text is paywalled, so we do not quote it and we name controls by id only. The table is our reading, in our words, of what each id asks a user of AI to be able to show, and the ids are mapped by us, not by ISO. It is not an official list.
| Id we map it to | Our reading | List in this guide |
|---|---|---|
| Clause 4.3 and A.4.2 | Know which AI systems and tools are in use, and keep a list of them. | Inventory; register rows not seen |
| A.9 | Each tool in use has an owner, an approval and a review. | Register quality |
| A.6.2.8 | Records of events are kept, and you can show the logging stayed on. | Recording by day |
| A.10.3 | Your suppliers are known and you hold what they can show about themselves. | Not scripted: a document you ask for |
On suppliers: Anthropic says it holds a certificate for ISO/IEC 42001:2023 for its AI management system (Anthropic: ISO 42001 announcement (read 2026-10-31)). A certificate is a document Anthropic holds, and you can ask for it as supplier evidence. It is listed in Anthropic’s Trust Center (trust.anthropic.com (read 2026-10-31)), where the certificate is available on request. It does not remove your own duty to know which tools your people added to Claude, which is what this guide checks.
This is evidence for your auditor. It is not a certification checklist, and it does not say any control is met.
What this covers, and what it does not
The Compliance API covers Claude Enterprise. For Claude Console and API-key workloads, the Activity Feed records administrative and resource events but no prompt or response text. Claude Code sessions authenticated with a Console API key, run through Amazon Bedrock, Google Cloud or Microsoft Foundry, or run as Claude Code cloud sessions are not captured (Anthropic: Compliance API FAQ (read 2026-10-31)). Recording starts when the Compliance API is first turned on and is not backfilled (Anthropic: Query the Activity Feed (read 2026-10-31)). An item added before that day is not in the feed, so it needs another source.
What you will have at the end
A Python script, ai_inventory_check.py, that reads your Claude Activity Feed pull and your AI register. It runs nine input checks, then builds four lists and prints each with its period, its filters, the run time, its row count and a SHA-256 of its rows. A reader with the same input and the same script gets the same row count and the same fingerprint. It also writes each list as a CSV. The full detail is in a PDF: Download the full guide (PDF).
- Inventory (4.3, A.4.2): every item in use during the period, meaning added or enabled in it, or added earlier and not removed before it began, marked
on register,UNREGISTEREDorUNREGISTERED (REMOVED LATER). - Register rows not seen (4.3, A.4.2): register rows the feed does not show in use during the period (
ON REGISTER, NOT SEEN) or whose last event by the period’s end is a removal (ON REGISTER, DELETED IN FEED). - Register quality (A.9): every register row with a status: owner, approval date, approved before first seen, review date.
- Recording by day (A.6.2.8): one row for each day of the period, marked
recorded,NO RECORDSorBEFORE ENABLED.
The records we ran it on are generated: a seeded stand-in, not a real organization’s data and not a measurement of anything. No real organization’s feed is public. The field names for the record itself come from Anthropic’s published Activity object, and the field that names each added item comes from the API reference’s object for that type: see step 2. We have not run the script on a real pull.
What you will need
- The Compliance API turned on for your organization, the day you first turned it on, and a key that can read the Activity Feed: a Compliance Access Key or an Admin API key with the
read:compliance_activitiesscope (Anthropic: Query the Activity Feed (read 2026-10-31)). - Your AI register as a CSV: for each item, its id as the feed shows it, its kind, its owner, the day it was approved and the day of its next review.
- Python 3 and the
duckdbmodule.
Step 1. Pull the feed from the day you turned the Compliance API on, once, as JSON lines
Each activity has an id, a created_at, an actor and a type (Anthropic: the Activity object (read 2026-10-31)). You can ask the feed for only some types with activity_types[], but pull everything, and pull it from the day you first turned the Compliance API on, not only the period. An item added before the period and still in use has its add event before the period began; without it, the item looks unbacked, and a connector added in the missing stretch is never seen. The script checks that the pull reaches back to the day you give as COMPLIANCE_API_ENABLED_ON (run 3 shows what happens when it does not). Set that day: without it the pull cannot be checked, and the check fails (run 4). If an earlier period found days with no records before this period and you explained them, list those days in KNOWN_GAP_DAYS. The recording-by-day list needs the days with no matching type too. Anthropic treats the feed as at-least-once and tells you to deduplicate on id, and to keep the starting cursor, the final last_id, the record count, the run time and the request-id of the final page with each pull (Anthropic: Delivery guarantees and completeness (read 2026-10-31)). The script reads a file and does not call the API. We did not call the real API, because we have no organization to call it on.
Step 2. Say which activity types add or remove an item, and load the feed and the register
Anthropic’s API reference lists the types for what gets added to Claude: MCP servers and connectors (mcp_server_created, mcp_server_deleted), integrations (claude_github_integration_created, claude_gdrive_integration_created), plugins (claude_plugin_created, claude_plugin_enabled), skills (claude_skill_created, claude_skill_enabled) and desktop extensions (desktop_extension_allowlisted), each with a matching removal type in the table (Anthropic: Query compliance activities, API reference (read 2026-10-31)). The script holds them in a table you can read and change. Each row gives the type, a kind, the field that names the item, and whether the type adds or removes. The reference also lists admin_connector_request_resolved and integration_user_connected, which carry mcp_server_id too; the script does not read them; add them to the table if you want them counted.
ITEM_TYPES = [
("mcp_server_created", "mcp_server", "mcp_server_id", "add"),
("mcp_server_deleted", "mcp_server", "mcp_server_id", "remove"),
("claude_github_integration_created", "integration", "integration_id", "add"),
("claude_github_integration_deleted", "integration", "integration_id", "remove"),
("claude_gdrive_integration_created", "integration", "integration_id", "add"),
("claude_gdrive_integration_deleted", "integration", "integration_id", "remove"),
("claude_plugin_created", "plugin", "plugin_id", "add"),
("claude_plugin_enabled", "plugin", "plugin_id", "add"),
("claude_plugin_deleted", "plugin", "plugin_id", "remove"),
("claude_skill_created", "skill", "skill_id", "add"),
("claude_skill_enabled", "skill", "skill_id", "add"),
("claude_skill_deleted", "skill", "skill_id", "remove"),
("desktop_extension_allowlisted", "desktop_extension", "extension_id", "add"),
("desktop_extension_removed_from_allowlist", "desktop_extension", "extension_id", "remove"),
]
The field names in the third column are the ones the API reference shows for each type. We have not seen a real record, so open one of each type and check before you rely on it. If a record has none, the input check below counts it.
DuckDB’s read_json takes format='newline_delimited' and a columns list; missing keys become NULL and keys you do not list are left out (DuckDB: loading JSON (read 2026-10-31)). The script builds that list from your table. Item ids in both files are lower-cased and trimmed, so MCP_SRV_02 in the register and mcp_srv_02 in the feed are the same item. The register is read as text, and a date counts only if it is written YYYY-MM-DD. DuckDB’s own cast is lenient: it reads 7/1/26 as a date in the year 7. So the script checks the form first, and any other date, such as 09/24/2026 or 7/1/26, becomes NULL instead of crashing the run, and an input check counts it.
DATE_FORM = r"\d{4}-\d{2}-\d{2}"
def strict_date(col):
"""YYYY-MM-DD only. DuckDB's own cast is lenient: it reads 8/5/26 as the year 8."""
c = f'trim(coalesce("{col}", \'\'))'
return f"CASE WHEN regexp_full_match({c}, '{DATE_FORM}') THEN try_strptime({c}, '%Y-%m-%d')::DATE END"
Step 3. Run nine input checks before building anything
A list built from an incomplete input looks complete. The script runs nine checks first, and each one names the planted fault that trips it. The checks on records cover every record up to the period’s end; a record with no readable timestamp always counts, because it cannot be placed. Faults in records after the period’s end do not mark the period incomplete: they are printed on an [info] line.
- Duplicate activity ids (a duplicated record).
- Records with no id or no readable
created_at(an unreadable timestamp). - The pull does not reach back to the day the Compliance API was turned on (a pull that starts at the period). If
COMPLIANCE_API_ENABLED_ONis not set, this cannot be checked, and the check fails instead (the enablement day not set). Quiet days right after enablement that you list inKNOWN_GAP_DAYSare skipped, so a pull that starts after them still reaches back. - Days with no records, from enablement (or the period’s start) to its end (five days with no records; a pull that starts at the period). When
COMPLIANCE_API_ENABLED_ONfalls before the period, the scan starts there, because the inventory relies on add events from before the period; otherwise it starts at the period’s first day. A day before the period that you list inKNOWN_GAP_DAYS, because you explained it in an earlier period, prints on an[info]line and does not fail. Any other day before the period still fails, and a day inside the period always counts, listed or not. - Days in the period before the Compliance API was enabled (the Compliance API turned on late).
- Add or remove records that name no item (an add record with no item id).
- Register rows with no item id (a register row with no item id).
- Register items listed more than once (a duplicated register row).
- Register dates that are not YYYY-MM-DD (a review date written 09/24/2026, an approval date written 7/1/26).
If any fails, all four lists are printed with INCOMPLETE in their heading. The script flags duplicate ids but does not remove them: remove them before you run it, keeping the first record for each id. It also prints, for information, the types in your table that never appear in the feed. A type that matches nothing means nothing was added that way, or the type is misspelled.
Step 4. Inventory: what the feed shows was added, matched to the register
The script works out each item’s state from every record up to the period’s end. An item is in use during the period if it was added or enabled in the period, or added earlier and not removed before the period began. Those items are joined to the register on the item id. An item in use and not on the register is UNREGISTERED; if its last event by the period’s end is a removal it reads UNREGISTERED (REMOVED LATER). For an item added before the period, the add-events column reads 0.
con.execute(f"""CREATE TABLE item_state AS SELECT kind, item,
min(ts) FILTER (WHERE action = 'add') AS first_seen,
count(*) FILTER (WHERE action = 'add' AND ts::DATE >= DATE '{p0}') AS adds_in_period,
arg_max(action, ts) FILTER (WHERE ts::DATE < DATE '{p0}') AS action_before,
arg_max(action, ts) AS last_action
FROM events WHERE item IS NOT NULL AND ts IS NOT NULL AND ts::DATE <= DATE '{p1}' GROUP BY 1, 2""")
con.execute("CREATE VIEW in_use AS SELECT * FROM item_state WHERE adds_in_period > 0 OR action_before = 'add'")
WITH reg AS (SELECT DISTINCT item FROM register WHERE item IS NOT NULL)
SELECT u.kind, u.item, u.first_seen::DATE AS first_seen, u.adds_in_period, u.last_action,
CASE WHEN r.item IS NOT NULL THEN 'on register'
WHEN u.last_action = 'remove' THEN 'UNREGISTERED (REMOVED LATER)' ELSE 'UNREGISTERED' END AS status
FROM in_use u LEFT JOIN reg r ON r.item = u.item
ORDER BY u.first_seen, u.item"""
Step 5. Register rows the feed does not back up, and register quality
The second list runs the other way. A register row the feed does not show in use during the period is ON REGISTER, NOT SEEN; one whose last event by the period’s end is a removal is ON REGISTER, DELETED IN FEED. Neither is proof of anything. The item may have been added before the feed started recording, or the register may be out of date, and the script asks you to find out which.
The third list gives every register row one status, the first that applies. NO OWNER, NO APPROVAL DATE and NO REVIEW DATE are blanks. APPROVAL DATE UNREADABLE and REVIEW DATE UNREADABLE are dates not written as a valid YYYY-MM-DD date, such as 7/1/26 or 09/24/2026. APPROVED AFTER FIRST SEEN means the approval date is later than the first add event the feed shows for that item. REVIEW OVERDUE means the next review date is before REVIEW_AS_OF. A row with none of these reads complete, which says only that the fields are filled and in order.
CASE WHEN r.item IS NULL THEN 'NO ITEM ID'
WHEN r.owner IS NULL THEN 'NO OWNER'
WHEN r.approved_text = '' THEN 'NO APPROVAL DATE'
WHEN r.approved_on IS NULL THEN 'APPROVAL DATE UNREADABLE'
WHEN r.review_text = '' THEN 'NO REVIEW DATE'
WHEN r.next_review IS NULL THEN 'REVIEW DATE UNREADABLE'
WHEN f.first_seen IS NOT NULL AND r.approved_on > f.first_seen THEN 'APPROVED AFTER FIRST SEEN'
WHEN r.next_review < DATE '{REVIEW_AS_OF}' THEN 'REVIEW OVERDUE'
ELSE 'complete' END AS status
Step 6. Recording by day, and a fingerprint on each list
The last list has one row for each day of the period with the number of records dated that day. It is the period’s own record; the input check above covers the longer stretch from enablement to the period’s end. A day with none that falls before COMPLIANCE_API_ENABLED_ON is BEFORE ENABLED, because Anthropic does not backfill; any other day with none is NO RECORDS. The logic is the one in our HIPAA audit logs guide, with its cautions: a quiet day can look like an outage, and a day with some records but not all is invisible to this check. Anthropic says activity volume is not a completeness check (Anthropic: Delivery guarantees and completeness (read 2026-10-31)).
Each list is printed with its period, filters, run time, row count and a SHA-256 over every row, in the order written to its CSV:
def fingerprint(rows):
body = "\n".join("|".join("" if v is None else str(v) for v in r) for r in rows)
return hashlib.sha256(body.encode()).hexdigest()
The fingerprint does not prove the rows are right. It lets anyone who re-runs the script on the same files confirm they got the same rows, and, if you keep the printed fingerprint apart from the CSV, lets you check the CSV has not changed.
Run 1: planted faults, and the script finds them
We ran the script on 2026-10-31 on generated records with faults planted: one record duplicated; one timestamp unreadable; every record from 2026-08-10 to 2026-08-14 removed; the Compliance API turned on on 2026-07-08, two days after the period began; an add record with no item id; and in the register, a connector in use that nobody registered (mcp_srv_03), a skill with no owner (skill_02), a plugin with no approval date, an extension approved 23 days after it was first seen (2026-07-28 and 2026-08-20), an integration whose review date had passed, a review date written 09/24/2026, an approval date written 7/1/26, a row with no item id, a duplicated row, a row for an item never seen (mcp_old_99), and a row for a server the feed shows deleted (mcp_srv_04). One register id is written MCP_SRV_02 , in capitals with a trailing space: it is not a fault, and the script should still match it. The script prints RESULT: EXCEPTIONS FOUND. It exits with code 0 unless you pass --strict, which exits 1 so a pipeline can stop.
python3 ai_inventory_check.py --plant
Output from our run 1
input : GENERATED records (not a real organization), seed 223, mode=plant
enabled on: 2026-07-08
== Input checks (records read: 482; before the period: 0, read for items added earlier and their removals; after the period's end: 19, not used)
[FAIL] duplicate activity ids: 1 (trips on: a duplicated record)
[FAIL] records with no id or no readable created_at: 1 (trips on: an unreadable timestamp)
[ok ] the pull does not reach back to the day the Compliance API was turned on: 0
[FAIL] days with no records, from enablement (or the period's start) to its end: 5 (trips on: five days with no records; a pull that starts at the period)
[FAIL] days in the period before the Compliance API was enabled: 2 (trips on: the Compliance API turned on late)
[FAIL] add or remove records that name no item: 1 (trips on: an add record with no item id)
[FAIL] register rows with no item id: 1 (trips on: a register row with no item id)
[FAIL] register items listed more than once: 1 (trips on: a duplicated register row)
[FAIL] register dates that are not YYYY-MM-DD: 2 (trips on: a review date written 09/24/2026, an approval date written 7/1/26)
earliest record: 2026-07-08; enabled on: 2026-07-08
days with no records: 2026-08-10..2026-08-14
days before enablement: 2026-07-06..2026-07-07
[info] after the period's end, not checked: duplicate ids 1, records with no id 0, add or remove records that name no item 0
[info] item types never seen in this feed: claude_github_integration_deleted, claude_gdrive_integration_deleted, claude_plugin_deleted, claude_skill_deleted, desktop_extension_removed_from_allowlist
== 4.3 / A.4.2: inventory ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : items in use during the period: an add or enable event in it, or an earlier one with no removal before it began (one row per kind and item; last action as of the period's end); matched to the register on the lower-cased, trimmed item id
run at : 2026-10-31 15:28:55 UTC
rows : 10
sha256 : 3c4e452af8fcebfd006d24a79b90f2f4b0107f31e89c48a67594c197a6036c67
mcp_server | mcp_srv_01 | 2026-07-08 | 1 | add | on register
integration | gh_int_01 | 2026-07-09 | 1 | add | on register
integration | gd_int_01 | 2026-07-14 | 1 | add | on register
mcp_server | mcp_srv_04 | 2026-07-15 | 1 | remove | on register
mcp_server | mcp_srv_02 | 2026-07-21 | 1 | add | on register
desktop_extension | ext_01 | 2026-07-28 | 1 | add | on register
skill | skill_01 | 2026-08-04 | 2 | add | on register
mcp_server | mcp_srv_03 | 2026-08-18 | 1 | add | UNREGISTERED
plugin | plugin_01 | 2026-08-19 | 2 | add | on register
skill | skill_02 | 2026-09-01 | 1 | add | on register
== 4.3 / A.4.2: register_not_seen ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : register rows the feed does not show in use during the period, or whose last event by the period's end is a removal
run at : 2026-10-31 15:28:55 UTC
rows : 2
sha256 : 9837749ec728c0d3a3eb13070cfaf63a59252589125795cdb4e956266aca7cbc
mcp_old_99 | mcp_server | data.platform@example.test | ON REGISTER, NOT SEEN
mcp_srv_04 | mcp_server | data.platform@example.test | ON REGISTER, DELETED IN FEED
== A.9: register_quality ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : every register row; first applicable status; review overdue means before 2026-09-27
run at : 2026-10-31 15:28:55 UTC
rows : 12
sha256 : f3a773cc740888cd2ac7fc1af0418a8782cd44279dcf3204ed108ff5401112b8
- | skill | data.platform@example.test | 2026-08-01 | 2026-12-01 | - | NO ITEM ID
ext_01 | desktop_extension | it.ops@example.test | 2026-08-20 | 2026-11-30 | 2026-07-28 | APPROVED AFTER FIRST SEEN
gd_int_01 | integration | data.platform@example.test | 2026-07-10 | - | 2026-07-14 | REVIEW DATE UNREADABLE
gh_int_01 | integration | eng.lead@example.test | 2026-07-02 | 2026-08-01 | 2026-07-09 | REVIEW OVERDUE
mcp_old_99 | mcp_server | data.platform@example.test | 2026-06-01 | 2026-12-01 | - | complete
mcp_srv_01 | mcp_server | data.platform@example.test | - | 2026-12-01 | 2026-07-08 | APPROVAL DATE UNREADABLE
mcp_srv_02 | mcp_server | data.platform@example.test | 2026-07-15 | 2026-12-01 | 2026-07-21 | complete
mcp_srv_04 | mcp_server | data.platform@example.test | 2026-07-01 | 2026-12-01 | 2026-07-15 | complete
plugin_01 | plugin | data.platform@example.test | - | 2026-12-01 | 2026-08-19 | NO APPROVAL DATE
skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete
skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete
skill_02 | skill | - | 2026-08-30 | 2026-12-01 | 2026-09-01 | NO OWNER
== A.6.2.8: recording_daily ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : one row per day; records counted by created_at date; enabled on 2026-07-08
run at : 2026-10-31 15:28:55 UTC
rows : 84
sha256 : a789403d14d1aa6cfe5bca300eef99458b52a3e27a64d5a7e0023670cc369cbe
days recorded: 77 of 84
NO RECORDS: 2026-08-10 to 2026-08-14 (5 day(s))
BEFORE ENABLED: 2026-07-06 to 2026-07-07 (2 day(s))
from 2026-07-06 to the period's end, days with no records: 5
== Result
input checks : FAILED, the lists above are not complete
follow-up : 17 item(s) need an explanation (unregistered items, register rows not seen, register rows incomplete, days with no records or before enablement)
RESULT: EXCEPTIONS FOUND
The result
The inventory has ten items. Nine are on the register, including mcp_srv_02, which matched despite its capitals and space. One, the MCP server mcp_srv_03, added on 2026-08-18, is UNREGISTERED.
| Kind | Item | First seen | Add events | Last action | Status |
|---|---|---|---|---|---|
| mcp_server | mcp_srv_01 | 2026-07-08 | 1 | add | on register |
| integration | gh_int_01 | 2026-07-09 | 1 | add | on register |
| integration | gd_int_01 | 2026-07-14 | 1 | add | on register |
| mcp_server | mcp_srv_04 | 2026-07-15 | 1 | remove | on register |
| mcp_server | mcp_srv_02 | 2026-07-21 | 1 | add | on register |
| desktop_extension | ext_01 | 2026-07-28 | 1 | add | on register |
| skill | skill_01 | 2026-08-04 | 2 | add | on register |
| mcp_server | mcp_srv_03 | 2026-08-18 | 1 | add | UNREGISTERED |
| plugin | plugin_01 | 2026-08-19 | 2 | add | on register |
| skill | skill_02 | 2026-09-01 | 1 | add | on register |
Two register rows are not backed by the feed: mcp_old_99 was never seen, and mcp_srv_04 was added on 2026-07-15, its last event in the feed is a removal, and it is still on the register.
| Item | Kind | Owner | Status |
|---|---|---|---|
| mcp_old_99 | mcp_server | data.platform@example.test | ON REGISTER, NOT SEEN |
| mcp_srv_04 | mcp_server | data.platform@example.test | ON REGISTER, DELETED IN FEED |
The register-quality list has twelve rows and seven carry a problem. The row with no item id is also counted by an input check. The duplicated skill_01 row reads complete twice, which is why a separate input check counts duplicates.
| Item | Kind | Owner | Approved on | Next review | First seen | Status |
|---|---|---|---|---|---|---|
| none | skill | data.platform@example.test | 2026-08-01 | 2026-12-01 | none | NO ITEM ID |
| ext_01 | desktop_extension | it.ops@example.test | 2026-08-20 | 2026-11-30 | 2026-07-28 | APPROVED AFTER FIRST SEEN |
| gd_int_01 | integration | data.platform@example.test | 2026-07-10 | none | 2026-07-14 | REVIEW DATE UNREADABLE |
| gh_int_01 | integration | eng.lead@example.test | 2026-07-02 | 2026-08-01 | 2026-07-09 | REVIEW OVERDUE |
| mcp_old_99 | mcp_server | data.platform@example.test | 2026-06-01 | 2026-12-01 | none | complete |
| mcp_srv_01 | mcp_server | data.platform@example.test | none | 2026-12-01 | 2026-07-08 | APPROVAL DATE UNREADABLE |
| mcp_srv_02 | mcp_server | data.platform@example.test | 2026-07-15 | 2026-12-01 | 2026-07-21 | complete |
| mcp_srv_04 | mcp_server | data.platform@example.test | 2026-07-01 | 2026-12-01 | 2026-07-15 | complete |
| plugin_01 | plugin | data.platform@example.test | none | 2026-12-01 | 2026-08-19 | NO APPROVAL DATE |
| skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete |
| skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete |
| skill_02 | skill | none | 2026-08-30 | 2026-12-01 | 2026-09-01 | NO OWNER |
The recording list shows 77 of 84 days recorded: five days (2026-08-10 to 2026-08-14) have no records, and the first two days are BEFORE ENABLED. Eight of the nine input checks fail, as planted; the pull starts on the day the Compliance API was turned on, so the reach-back check passes. All four lists are marked INCOMPLETE. Seventeen items need an explanation.
Run 2: the clean records, and the script passes
The same script on the same seed with --plant left off. The counts differ from run 1 because planting adds and removes records, and because the clean feed starts on 2026-06-01, the day the stand-in organization turned the Compliance API on. All nine input checks pass. The file also holds three days after the period’s end with one duplicated record: the script reports it on an [info] line and the period stays clean. All ten items in use are on the register, including gh_int_00, added on 2026-06-15 before the period began and still in use: its add-events column reads 0 and it is not a finding. No register row is unbacked, all ten register rows are complete, and all 84 days are recorded. The script prints RESULT: NOTHING TO FOLLOW UP.
python3 ai_inventory_check.py
Output from our run 2
input : GENERATED records (not a real organization), seed 223, mode=clean
enabled on: 2026-06-01
== Input checks (records read: 737; before the period: 222, read for items added earlier and their removals; after the period's end: 19, not used)
[ok ] duplicate activity ids: 0
[ok ] records with no id or no readable created_at: 0
[ok ] the pull does not reach back to the day the Compliance API was turned on: 0
[ok ] days with no records, from enablement (or the period's start) to its end: 0
[ok ] days in the period before the Compliance API was enabled: 0
[ok ] add or remove records that name no item: 0
[ok ] register rows with no item id: 0
[ok ] register items listed more than once: 0
[ok ] register dates that are not YYYY-MM-DD: 0
earliest record: 2026-06-01; enabled on: 2026-06-01
[info] after the period's end, not checked: duplicate ids 1, records with no id 0, add or remove records that name no item 0
[info] item types never seen in this feed: mcp_server_deleted, claude_github_integration_deleted, claude_gdrive_integration_deleted, claude_plugin_deleted, claude_skill_deleted, desktop_extension_removed_from_allowlist
== 4.3 / A.4.2: inventory
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : items in use during the period: an add or enable event in it, or an earlier one with no removal before it began (one row per kind and item; last action as of the period's end); matched to the register on the lower-cased, trimmed item id
run at : 2026-10-31 15:28:56 UTC
rows : 10
sha256 : cc27f875f755da4e018be1f6fd8b9b97ab0155a06f8129df8e0d48168400f9ba
integration | gh_int_00 | 2026-06-15 | 0 | add | on register
mcp_server | mcp_srv_01 | 2026-07-08 | 1 | add | on register
integration | gh_int_01 | 2026-07-09 | 1 | add | on register
integration | gd_int_01 | 2026-07-14 | 1 | add | on register
mcp_server | mcp_srv_02 | 2026-07-21 | 1 | add | on register
desktop_extension | ext_01 | 2026-07-28 | 1 | add | on register
skill | skill_01 | 2026-08-04 | 2 | add | on register
mcp_server | mcp_srv_03 | 2026-08-18 | 1 | add | on register
plugin | plugin_01 | 2026-08-19 | 2 | add | on register
skill | skill_02 | 2026-09-01 | 1 | add | on register
== 4.3 / A.4.2: register_not_seen
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : register rows the feed does not show in use during the period, or whose last event by the period's end is a removal
run at : 2026-10-31 15:28:56 UTC
rows : 0
sha256 : e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
== A.9: register_quality
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : every register row; first applicable status; review overdue means before 2026-09-27
run at : 2026-10-31 15:28:56 UTC
rows : 10
sha256 : 3334a217671d883cc9ad2857d3e7c642f6ebc8254e6afa421c0f0c228642957f
ext_01 | desktop_extension | it.ops@example.test | 2026-07-20 | 2026-11-30 | 2026-07-28 | complete
gd_int_01 | integration | data.platform@example.test | 2026-07-10 | 2026-12-15 | 2026-07-14 | complete
gh_int_00 | integration | eng.lead@example.test | 2026-06-10 | 2026-12-15 | 2026-06-15 | complete
gh_int_01 | integration | eng.lead@example.test | 2026-07-02 | 2026-12-15 | 2026-07-09 | complete
mcp_srv_01 | mcp_server | data.platform@example.test | 2026-07-01 | 2026-12-01 | 2026-07-08 | complete
mcp_srv_02 | mcp_server | data.platform@example.test | 2026-07-15 | 2026-12-01 | 2026-07-21 | complete
mcp_srv_03 | mcp_server | data.platform@example.test | 2026-08-10 | 2026-12-01 | 2026-08-18 | complete
plugin_01 | plugin | data.platform@example.test | 2026-08-15 | 2026-12-01 | 2026-08-19 | complete
skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete
skill_02 | skill | ops.manager@example.test | 2026-08-30 | 2026-12-01 | 2026-09-01 | complete
== A.6.2.8: recording_daily
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : one row per day; records counted by created_at date; enabled on 2026-06-01
run at : 2026-10-31 15:28:56 UTC
rows : 84
sha256 : eb2ca3483c5fab1cc24b37b6f7de33f862814f4a1b0c0dfb41a9d073102480f8
days recorded: 84 of 84
from 2026-06-01 to the period's end, days with no records: 0
== Result
input checks : all passed
follow-up : 0 item(s) need an explanation (unregistered items, register rows not seen, register rows incomplete, days with no records or before enablement)
RESULT: NOTHING TO FOLLOW UP
Run 3: the pull starts at the period, and the script says so
The same clean records, pulled from the period’s first day instead of the day the stand-in organization turned the Compliance API on (2026-06-01). In the missing stretch we added an MCP server, mcp_srv_00, on 2026-06-20 and left it off the register. The inventory cannot see it, because its add event is not in the pull. The input checks catch the pull instead: it does not reach back to 2026-06-01, and the 35 days from 2026-06-01 to 2026-07-05 have no records. gh_int_00, added in that stretch and on the register, reads ON REGISTER, NOT SEEN. The script prints RESULT: EXCEPTIONS FOUND, not NOTHING TO FOLLOW UP.
python3 ai_inventory_check.py --short-pull
Output from our run 3
input : GENERATED records (not a real organization), seed 223, mode=short
enabled on: 2026-06-01
== Input checks (records read: 515; before the period: 0, read for items added earlier and their removals; after the period's end: 19, not used)
[ok ] duplicate activity ids: 0
[ok ] records with no id or no readable created_at: 0
[FAIL] the pull does not reach back to the day the Compliance API was turned on: 1 (trips on: a pull that starts at the period; the enablement day not set)
[FAIL] days with no records, from enablement (or the period's start) to its end: 35 (trips on: five days with no records; a pull that starts at the period)
[ok ] days in the period before the Compliance API was enabled: 0
[ok ] add or remove records that name no item: 0
[ok ] register rows with no item id: 0
[ok ] register items listed more than once: 0
[ok ] register dates that are not YYYY-MM-DD: 0
earliest record: 2026-07-06; enabled on: 2026-06-01
days with no records: 2026-06-01..2026-07-05
[info] after the period's end, not checked: duplicate ids 1, records with no id 0, add or remove records that name no item 0
[info] item types never seen in this feed: mcp_server_deleted, claude_github_integration_deleted, claude_gdrive_integration_deleted, claude_plugin_deleted, claude_skill_deleted, desktop_extension_removed_from_allowlist
== 4.3 / A.4.2: inventory ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : items in use during the period: an add or enable event in it, or an earlier one with no removal before it began (one row per kind and item; last action as of the period's end); matched to the register on the lower-cased, trimmed item id
run at : 2026-10-31 15:28:56 UTC
rows : 9
sha256 : 80786ed816dfb83b7b85427455e0d257b91a5143bf5b186c8daccd6bce43dddb
mcp_server | mcp_srv_01 | 2026-07-08 | 1 | add | on register
integration | gh_int_01 | 2026-07-09 | 1 | add | on register
integration | gd_int_01 | 2026-07-14 | 1 | add | on register
mcp_server | mcp_srv_02 | 2026-07-21 | 1 | add | on register
desktop_extension | ext_01 | 2026-07-28 | 1 | add | on register
skill | skill_01 | 2026-08-04 | 2 | add | on register
mcp_server | mcp_srv_03 | 2026-08-18 | 1 | add | on register
plugin | plugin_01 | 2026-08-19 | 2 | add | on register
skill | skill_02 | 2026-09-01 | 1 | add | on register
== 4.3 / A.4.2: register_not_seen ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : register rows the feed does not show in use during the period, or whose last event by the period's end is a removal
run at : 2026-10-31 15:28:56 UTC
rows : 1
sha256 : d5177d627452797142f2a2b230b87409eba8a10dee77a05790948dad669b10fc
gh_int_00 | integration | eng.lead@example.test | ON REGISTER, NOT SEEN
== A.9: register_quality ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : every register row; first applicable status; review overdue means before 2026-09-27
run at : 2026-10-31 15:28:56 UTC
rows : 10
sha256 : a79ae3f82a4eb3771058a46f10babd90937ad762d17a46db917ec3dca15bfe64
ext_01 | desktop_extension | it.ops@example.test | 2026-07-20 | 2026-11-30 | 2026-07-28 | complete
gd_int_01 | integration | data.platform@example.test | 2026-07-10 | 2026-12-15 | 2026-07-14 | complete
gh_int_00 | integration | eng.lead@example.test | 2026-06-10 | 2026-12-15 | - | complete
gh_int_01 | integration | eng.lead@example.test | 2026-07-02 | 2026-12-15 | 2026-07-09 | complete
mcp_srv_01 | mcp_server | data.platform@example.test | 2026-07-01 | 2026-12-01 | 2026-07-08 | complete
mcp_srv_02 | mcp_server | data.platform@example.test | 2026-07-15 | 2026-12-01 | 2026-07-21 | complete
mcp_srv_03 | mcp_server | data.platform@example.test | 2026-08-10 | 2026-12-01 | 2026-08-18 | complete
plugin_01 | plugin | data.platform@example.test | 2026-08-15 | 2026-12-01 | 2026-08-19 | complete
skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete
skill_02 | skill | ops.manager@example.test | 2026-08-30 | 2026-12-01 | 2026-09-01 | complete
== A.6.2.8: recording_daily ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : one row per day; records counted by created_at date; enabled on 2026-06-01
run at : 2026-10-31 15:28:56 UTC
rows : 84
sha256 : eb2ca3483c5fab1cc24b37b6f7de33f862814f4a1b0c0dfb41a9d073102480f8
days recorded: 84 of 84
from 2026-06-01 to the period's end, days with no records: 35
== Result
input checks : FAILED, the lists above are not complete
follow-up : 36 item(s) need an explanation (unregistered items, register rows not seen, register rows incomplete, days with no records or before enablement)
RESULT: EXCEPTIONS FOUND
Run 4: the enablement day is not set, and the script will not pass
The same clean records as run 2, with COMPLIANCE_API_ENABLED_ON left unset. Nothing else is wrong, but without that day the script cannot tell whether the pull reaches back far enough, so it fails that check, marks the lists INCOMPLETE and prints RESULT: EXCEPTIONS FOUND. It never prints NOTHING TO FOLLOW UP without an enablement day.
python3 ai_inventory_check.py --enablement-unknown
Output from our run 4
input : GENERATED records (not a real organization), seed 223, mode=clean
enabled on: not set (COMPLIANCE_API_ENABLED_ON is None)
== Input checks (records read: 737; before the period: 222, read for items added earlier and their removals; after the period's end: 19, not used)
[ok ] duplicate activity ids: 0
[ok ] records with no id or no readable created_at: 0
[FAIL] the day the Compliance API was turned on is not set, so the pull cannot be checked: 1 (trips on: a pull that starts at the period; the enablement day not set)
[ok ] days with no records, from enablement (or the period's start) to its end: 0
[ok ] days in the period before the Compliance API was enabled: 0
[ok ] add or remove records that name no item: 0
[ok ] register rows with no item id: 0
[ok ] register items listed more than once: 0
[ok ] register dates that are not YYYY-MM-DD: 0
earliest record: 2026-06-01; enabled on: not set
[info] after the period's end, not checked: duplicate ids 1, records with no id 0, add or remove records that name no item 0
[info] item types never seen in this feed: mcp_server_deleted, claude_github_integration_deleted, claude_gdrive_integration_deleted, claude_plugin_deleted, claude_skill_deleted, desktop_extension_removed_from_allowlist
== 4.3 / A.4.2: inventory ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : items in use during the period: an add or enable event in it, or an earlier one with no removal before it began (one row per kind and item; last action as of the period's end); matched to the register on the lower-cased, trimmed item id
run at : 2026-10-31 15:28:56 UTC
rows : 10
sha256 : cc27f875f755da4e018be1f6fd8b9b97ab0155a06f8129df8e0d48168400f9ba
integration | gh_int_00 | 2026-06-15 | 0 | add | on register
mcp_server | mcp_srv_01 | 2026-07-08 | 1 | add | on register
integration | gh_int_01 | 2026-07-09 | 1 | add | on register
integration | gd_int_01 | 2026-07-14 | 1 | add | on register
mcp_server | mcp_srv_02 | 2026-07-21 | 1 | add | on register
desktop_extension | ext_01 | 2026-07-28 | 1 | add | on register
skill | skill_01 | 2026-08-04 | 2 | add | on register
mcp_server | mcp_srv_03 | 2026-08-18 | 1 | add | on register
plugin | plugin_01 | 2026-08-19 | 2 | add | on register
skill | skill_02 | 2026-09-01 | 1 | add | on register
== 4.3 / A.4.2: register_not_seen ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : register rows the feed does not show in use during the period, or whose last event by the period's end is a removal
run at : 2026-10-31 15:28:56 UTC
rows : 0
sha256 : e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
== A.9: register_quality ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : every register row; first applicable status; review overdue means before 2026-09-27
run at : 2026-10-31 15:28:56 UTC
rows : 10
sha256 : 3334a217671d883cc9ad2857d3e7c642f6ebc8254e6afa421c0f0c228642957f
ext_01 | desktop_extension | it.ops@example.test | 2026-07-20 | 2026-11-30 | 2026-07-28 | complete
gd_int_01 | integration | data.platform@example.test | 2026-07-10 | 2026-12-15 | 2026-07-14 | complete
gh_int_00 | integration | eng.lead@example.test | 2026-06-10 | 2026-12-15 | 2026-06-15 | complete
gh_int_01 | integration | eng.lead@example.test | 2026-07-02 | 2026-12-15 | 2026-07-09 | complete
mcp_srv_01 | mcp_server | data.platform@example.test | 2026-07-01 | 2026-12-01 | 2026-07-08 | complete
mcp_srv_02 | mcp_server | data.platform@example.test | 2026-07-15 | 2026-12-01 | 2026-07-21 | complete
mcp_srv_03 | mcp_server | data.platform@example.test | 2026-08-10 | 2026-12-01 | 2026-08-18 | complete
plugin_01 | plugin | data.platform@example.test | 2026-08-15 | 2026-12-01 | 2026-08-19 | complete
skill_01 | skill | ops.manager@example.test | 2026-08-03 | 2026-12-01 | 2026-08-04 | complete
skill_02 | skill | ops.manager@example.test | 2026-08-30 | 2026-12-01 | 2026-09-01 | complete
== A.6.2.8: recording_daily ** INCOMPLETE: see input checks **
period : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters : one row per day; records counted by created_at date; enabled on not set
run at : 2026-10-31 15:28:56 UTC
rows : 84
sha256 : eb2ca3483c5fab1cc24b37b6f7de33f862814f4a1b0c0dfb41a9d073102480f8
days recorded: 84 of 84
from 2026-07-06 to the period's end, days with no records: 0
== Result
input checks : FAILED, the lists above are not complete
follow-up : 0 item(s) need an explanation (unregistered items, register rows not seen, register rows incomplete, days with no records or before enablement)
RESULT: EXCEPTIONS FOUND
Every row count and SHA-256 above is repeatable: the records are seeded, so the same script gives the same fingerprints (the run time line changes). “Nothing to follow up” says only that the input passed its checks and that no item, register row or day needed an explanation. It does not say a requirement is satisfied, and it does not test whether anyone reads the register.
Run it on your own data
To run this on your own organization, save your Activity Feed pull, from the day you first turned the Compliance API on, as one JSON object per line and your AI register as a CSV file with a header row, one row per item, with each item written as the id your feed shows and every date written YYYY-MM-DD. At the top of the script, set ACTIVITIES_FILE and REGISTER_FILE to those file paths, PERIOD_START and PERIOD_END to the period you are evidencing, COMPLIANCE_API_ENABLED_ON to the day you first turned the Compliance API on (the run fails a check without it), KNOWN_GAP_DAYS to any days before the period with no records that you explained in an earlier period (leave it empty if there are none), REVIEW_AS_OF to the day your review dates are measured against, and REG_ITEM_COL, REG_KIND_COL, REG_OWNER_COL, REG_APPROVED_COL and REG_REVIEW_COL to your CSV column names. In ITEM_TYPES, change the third value of a line to the name of the field that holds the item's id in your own records of that type. Change nothing else.
We also ran the script on a second set of generated files with different file and column names and a changed field name in two lines of the table (server_id in place of mcp_server_id), pulled from a day before the period. It read a server added before the period as on the register, reported a duplicated record after the period’s end as information only, and reported each fault we planted in that set, including an approval date written 6/20/26. It also had two days with no records before the period: one the reader had explained earlier and listed in KNOWN_GAP_DAYS, printed as information, and one they had not, which failed the check. We have not run it on a real pull.
Method
- Input: generated Activity Feed records, written by the script as JSON lines in the Activity object’s shape, with a fixed seed (223), plus a generated AI register. Fields used:
id,created_at,type,actor.type,actor.email_address,actor.user_id(Anthropic: Activity object (read 2026-10-31)) and, for the item, the field names the API reference shows for each type (step 2). - Period: 2026-07-06 to 2026-09-27, UTC, inclusive. Days are counted by the
created_atdate in UTC. Records before the period are read to find items added earlier and their removals. Records after the period’s end are not used, so the lists show the state on the last day of the period; faults in them are printed as information and do not fail a check. - Settings you can disagree with are written at the top of the script: which types count as an add or a removal, the day your review dates are measured against, and the days before the period you have already explained (
KNOWN_GAP_DAYS). - A problem row is counted by an input check, and a failed check marks the lists INCOMPLETE. Records of types not in the table count toward the input checks and the recording-by-day list, never toward the inventory.
Limits
- This is not a client result. The records are generated. The run shows the script finds planted faults. It says nothing about any real organization or how often such faults happen.
- We did not call the Compliance API, and we have not seen a real record. The field names come from the API reference, so check one record of each type.
- The feed shows what was added to Claude, not every AI tool your company uses. Coverage: The Compliance API covers Claude Enterprise. For Claude Console and API-key workloads, the Activity Feed records administrative and resource events but no prompt or response text. Claude Code sessions authenticated with a Console API key, run through Amazon Bedrock, Google Cloud or Microsoft Foundry, or run as Claude Code cloud sessions are not captured (Anthropic: Compliance API FAQ (read 2026-10-31)). Recording starts when the Compliance API is first turned on.
- The script does not read who added an item. In our generated records, a skill or plugin enabled by a person is an add event like any other. The script does not tell an approved enablement from an unapproved one; it checks that the item is on the register.
- Our reading of the standard is not ISO’s. The table in the first section is ours. Your auditor and your own scope decide what the standard asks of you.
- Disabling a plugin or skill (
claude_plugin_disabled,claude_skill_disabled) is not counted as a removal. Anthropic records each for one member’s account (Anthropic: API reference (read 2026-10-31)), so one member turning it off does not mean the organization stopped using it. Add those types to the table if you want it to be. - A register row marked
completeis a row with its fields filled in. It does not show anyone reviewed the tool. - The pull holds workforce emails, so store it under the access controls you use for other security records. The script runs on your machine and sends nothing to Data Meaning.
Tested on
Tested on
- Platform
- DuckDB 1.5.6, Python 3.14, run from Python with the
duckdbmodule - Edition
- Open-source DuckDB, in-memory
- Date
- 2026-10-31
- Dataset
- Generated records, seed 223 (not a real organization). Field names from Anthropic’s Activity object (read 2026-10-31); activity types from Anthropic’s API reference (read 2026-10-31)
- Code
- ai_inventory_check.py
When this is not enough
- You need every period, not one run. The script is a point-in-time pull. Keep each pull’s parameters and the lists as they were produced.
- Claude is used outside the Compliance API’s coverage, or other AI tools are in use. Those need their own sources.
- The requirement is more than a list. Your policy, your impact assessments, your supplier records and what you did about each finding sit in your own records, not in the feed.
- An audit is a judgment. Whether a list is enough is for your auditor and your own counsel.
See Control Tower for Claude
Control Tower for Claude runs in your own Azure subscription.