Check recipe

NIST AI RMF and Claude: what you can show, and a script to check it

DuckDB2026-10-31Script and PDF downloadable

The short answer

A company using Claude Enterprise can show three records: an AI incident log with the nine example fields NIST lists, a register of Claude use cases that each state a context of use, and Activity Feed records by day from the day recording began, with any day that has none listed. This guide’s script checks those three records and lists the gaps.

What NIST suggests, and what this guide checks

The NIST AI Risk Management Framework is voluntary. NIST does not certify or score it, so there is nothing to pass, and a company can only show what it keeps and what it does. NIST suggests two practices that a company using Claude can keep records of: criteria for reporting incidents, and a documented context of use. We turned them into an incident log and a use-case register, and this guide checks both. The incident log comes from the Generative AI Profile (NIST AI 600-1 (read 2026-10-31)), whose action GV-4.3-002 under GOVERN 4.3 reads: “Establish organizational practices to identify the minimum set of criteria necessary for GAI system incident reporting such as: System ID (auto-generated most likely), Title, Reporter, System/Source, Data Reported, Date of Incident, Description, Impact(s), Stakeholder(s) Impacted.” The use-case register comes from action MP-1.1-002 under MAP 1.1, which begins: “Determine and document the expected and acceptable GAI system context of use” and goes on to name what to assess. NIST labels each action in the profile a suggested action.

The NIST ids this guide uses, and the list that follows each
NIST idWhat NIST saysList in this guide
GOVERN 4.3, action GV-4.3-002“Organizational practices are in place to enable AI testing, identification of incidents, and information sharing” (NIST: AI RMF Core (read 2026-10-31)). The action names the nine fields above.Incident log check
MAP 1.1, action MP-1.1-002Intended purposes, uses and “prospective settings in which the AI system will be deployed are understood and documented” (NIST: AI RMF Core (read 2026-10-31)).Use-case register check
GOVERN 1.5 and MANAGE 4.1“Ongoing monitoring and periodic review of the risk management process and its outcomes are planned”; post-deployment monitoring plans “are implemented” (NIST: AI RMF Core (read 2026-10-31)).Recording by day
GOVERN 1.6“Mechanisms are in place to inventory AI systems” (NIST: AI RMF Core (read 2026-10-31)).Not rebuilt here: see our AI inventory guide
GOVERN 6.1Policies for AI risks from third-party entities (NIST: AI RMF Core (read 2026-10-31)).Not scripted: a document you ask Anthropic for

On the third party, Anthropic publishes documents about itself in its Trust Center (trust.anthropic.com (read 2026-10-31)). Ask for the ones your policy under GOVERN 6.1 names. They are Anthropic’s documents, and they say nothing about your own use.

The Activity Feed fields this script reads (id, created_at, type) carry no incident report and no use-case id, so the incident log and the register come from records your own company keeps. Two of the three lists check those records. The third checks the Activity Feed, and shows only that its recording ran. It does not show that anyone reviewed what it recorded: a monitoring plan, who reads the feed and what they did about it are yours to write down.

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)). A day before that has no records, and the script says so instead of calling it a gap you could have closed.

What you will have at the end

A Python script, nist_ai_rmf_claude_evidence.py, that reads your Claude Activity Feed pull, your incident log and your use-case register. It pins the two NIST documents, runs ten input checks, then builds three 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).

  • Incident log check (GOVERN 4.3, GV-4.3-002): every row of your log with a status: MISSING followed by the NIST fields that are blank, NOT WRITTEN YYYY-MM-DD, INCIDENT DATED AFTER IT WAS REPORTED, SYSTEM NOT ON REGISTER, or complete. A row reads complete when it holds all nine fields, readable dates in order and a System ID on the register. It does not mean the log holds every incident.
  • Use-case register check (MAP 1.1, MP-1.1-002): every register row with a status (NO USE CASE ID, NO CONTEXT OF USE, NO INTENDED PURPOSE, NO OWNER, NO RISK TIER, NO REVIEW DATE, NOT WRITTEN YYYY-MM-DD, REVIEW OVERDUE or complete) and a separate mark, FOR LEGAL TO CHECK, on HR and people use cases.
  • Recording by day (GOVERN 1.5, MANAGE 4.1): one row for each day of the period, marked recorded, NO RECORDS or BEFORE 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. We had no access to a real organization’s feed, incident log or register. The Activity Feed fields the script reads (id, created_at, type) come from Anthropic’s Activity object (Anthropic: Query the Activity Feed (read 2026-10-31)). 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_activities scope (Anthropic: Query the Activity Feed (read 2026-10-31)). The script reads a file and does not call the API: you pull the feed yourself, as one JSON object per line, from the day you first turned the Compliance API on.
  • Your AI incident log as a CSV with a header row, one row per incident, with a column for each of NIST’s nine fields. Keep it in your own system of record; it is not in the feed.
  • Your Claude use-case register as a CSV: for each use case an id, its name, the department, the intended purpose, the context of use, an owner, a risk tier and the day it was last reviewed.
  • Python 3 and the duckdb module.

Step 1. Pin NIST’s text, and set what you can disagree with

The script downloads both NIST documents and prints each one’s size, its Last-Modified header and its SHA-256. If either differs from the pinned size or hash, an input check fails, so you know NIST re-published the file and the quoted action text needs reading again. The pinned files are the July 2024 NIST AI 600-1 (read 2026-10-31) and NIST AI 100-1 (read 2026-10-31), the AI RMF 1.0. If the download fails, the check fails too: a check that could not run is not a pass.

def pin_nist():
    """Download each NIST document once, print its size, Last-Modified and SHA-256, return the number that do not match."""
    bad = 0
    for label, url, size, sha in NIST_DOCS:
        try:
            req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
            with urllib.request.urlopen(req, timeout=60) as r:
                body, modified = r.read(), r.headers.get("Last-Modified", "not sent")
        except (OSError, http.client.HTTPException) as e:          # a cut-off download is a failed pin, not a crash
            print(f"  {label}: could not be fetched ({e.__class__.__name__}); the pin cannot be checked")
            bad += 1
            continue
        got = hashlib.sha256(body).hexdigest()
        same = len(body) == size and got == sha
        bad += 0 if same else 1
        print(f"  {label}: {len(body)} bytes, Last-Modified {modified}, sha256 {got}" + ("" if same else "   ** DIFFERENT FROM THE PIN **"))
    return bad

Everything you can disagree with sits in one block at the top. The period, the review interval (180 days is our setting, not NIST’s: NIST names none), the column of your files that holds each field, and the words that mark an HR use case. NIST’s own text reads “Data Reported”. The script reads that field as the date the incident was reported and checks it as a date; change the column or the check if you read it differently. NIST says the System ID is “auto-generated most likely”; the script accepts any text you use as the id.

# ---- CHANGE THESE FOR YOUR OWN DATA ---------------------------------------------------------
PERIOD_START = "2026-07-06"          # first day of the period you are evidencing (UTC, inclusive)
PERIOD_END = "2026-09-27"            # last day of the period (UTC, inclusive)
COMPLIANCE_API_ENABLED_ON = None     # the day you first turned the Compliance API on, YYYY-MM-DD; None fails a check
KNOWN_GAP_DAYS = []                  # days BEFORE the period with no records that you explained earlier, YYYY-MM-DD
ACTIVITIES_FILE = None               # your Activity Feed pull, one JSON object per line; None = generate
INCIDENTS_FILE = None                # your AI incident log (CSV with a header row); None = generate
USE_CASES_FILE = None                # your Claude use-case register (CSV with a header row); None = generate
REVIEW_AS_OF = "2026-09-27"          # the day review ages are counted from
REVIEW_MAX_DAYS = 180                # your own policy: a register row not reviewed within this many days is overdue
# the column of YOUR incident log that holds each of NIST's nine fields (the defaults are NIST's own words)
INC_COLS = {
    "system_id": "System ID", "title": "Title", "reporter": "Reporter", "system_source": "System/Source",
    "date_reported": "Data Reported", "date_of_incident": "Date of Incident", "description": "Description",
    "impacts": "Impact(s)", "stakeholders": "Stakeholder(s) Impacted",
}
# the column of YOUR register that holds each of its fields
UC_COLS = {
    "use_case_id": "use_case_id", "use_case": "use_case", "department": "department",
    "intended_purpose": "intended_purpose", "context_of_use": "context_of_use", "owner": "owner",
    "risk_tier": "risk_tier", "last_reviewed": "last_reviewed",
}
# HR and people use cases are listed for Legal; change the words to fit your register
HR_TERMS = ["hiring", "recruit", "resume", "candidate", "promotion", "performance review", "termination",
            "employee monitoring"]                       # matched inside use_case and intended_purpose, any case
HR_DEPARTMENTS = ["hr", "human resources", "people", "talent"]  # matched against the whole department value, any case
LEGAL_REASON = ("laws on automated employment decisions, such as NYC Local Law 144, Colorado SB 26-189 (bill text not "
                "verified) and California's automated decisionmaking technology rules under the CCPA, may apply. "
                "Other laws may also apply")
# ----------------------------------------------------------------------------------------------

The generated incident log uses NIST’s own words as its column names, so a log written that way needs no column changes. Column names are matched without regard to case, because DuckDB identifiers are case-insensitive, quoted ones too (DuckDB: keywords and identifiers (read 2026-10-31)). If a name in the block does not exist in your file, the script stops and prints the columns it did find.

Step 2. Read dates strictly

Files are read as text with all_varchar and a header row (DuckDB: CSV overview (read 2026-10-31)). DuckDB’s own cast to a date is lenient: it reads 8/5/26 as the year 8 and accepts 2026/09/24. So the script checks the form first and only then parses; try_strptime returns NULL when the text does not match the format (DuckDB: date format functions (read 2026-10-31)). A date in any other form becomes NULL, and the row is listed with NOT WRITTEN YYYY-MM-DD. No row is dropped for a date it could not read.

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({qcol(col)}, ''))"
    return f"CASE WHEN regexp_full_match({c}, '{DATE_FORM}') THEN try_strptime({c}, '%Y-%m-%d')::DATE END"

Step 3. Run ten input checks before building anything

A list built from an incomplete input looks complete. The script runs ten checks first, and each prints the planted fault that trips it. If any fails, all three lists are printed with INCOMPLETE in their headings.

  • NIST documents that are not the pinned file (a re-published file, or a failed download).
  • Duplicate activity ids (a duplicated record).
  • Activity 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_ON is not set, it cannot be checked, and the check fails instead.
  • Days with no records, from enablement (or the period’s start) to its end (days removed from the feed). Days before the period that you explained earlier go in KNOWN_GAP_DAYS and print as information; a day inside the period always counts.
  • Days in the period before the Compliance API was enabled (the Compliance API turned on late).
  • Incident log rows repeated, with the same System ID, Title and Date of Incident (a duplicated incident row).
  • Register ids listed more than once (a duplicated register row).
  • Register rows with no use case id (a register row with no id).
  • Incident log and register dates not written YYYY-MM-DD (a short date such as 8/5/26). The row keeps its own status too, so it is never dropped.

The script flags duplicates and does not remove them. Ids in both files are lower-cased and trimmed before they are matched, so uc-001  in the log and UC-001 in the register are the same use case.

Step 4. The incident log against NIST’s nine fields

NIST gives the nine fields as examples (“such as”). The script checks all nine because we chose to. Every row of the log is listed. None is filtered out by date, because an incident with an unreadable date is the one you most need to see. A blank field counts as missing after trimming. A row gets every status that applies, joined with a semicolon. The output shows the row’s place in the file, the System ID and the Date of Incident, and never the reporter, the title or the description, so the list carries no person’s name.

    missing = ", ".join(f"CASE WHEN {k}{'_text' if k in INC_DATES else ''} {'= ' + chr(39) * 2 if k in INC_DATES else 'IS NULL'} THEN '{INC_LABELS[k]}' END"
                        for k in INC_LABELS)
    unread = ", ".join(f"CASE WHEN {k}_text <> '' AND {k} IS NULL THEN '{INC_LABELS[k]}' END" for k in INC_DATES)
    sql = f"""
        WITH reg AS (SELECT DISTINCT use_case_id FROM register WHERE use_case_id IS NOT NULL),
        i AS (SELECT n.*, concat_ws(', ', {missing}) AS missing, concat_ws(', ', {unread}) AS unread,
                     r.use_case_id IS NULL AS off_register
              FROM (SELECT *, lower(system_id) AS sid FROM incidents) n LEFT JOIN reg r ON r.use_case_id = n.sid)
        SELECT row_no, coalesce(system_id, '-') AS system_id, date_of_incident,
               coalesce(nullif(concat_ws('; ',
                   CASE WHEN missing <> '' THEN 'MISSING: ' || missing END,
                   CASE WHEN unread <> '' THEN 'NOT WRITTEN YYYY-MM-DD: ' || unread END,
                   CASE WHEN system_id IS NOT NULL AND off_register THEN 'SYSTEM NOT ON REGISTER' END,
                   CASE WHEN date_of_incident > date_reported THEN 'INCIDENT DATED AFTER IT WAS REPORTED' END), ''), 'complete') AS status
        FROM i ORDER BY row_no"""

Step 5. The register against a documented context of use

MP-1.1-002 asks for the expected and acceptable context of use to be determined and documented, with domain experts. A script cannot tell whether a context of use is good. It can tell that one is written down. Each row gets the first status that applies: no id, no context of use, no intended purpose, no owner, no risk tier, no review date, a review date not written YYYY-MM-DD, or a review older than REVIEW_MAX_DAYS before REVIEW_AS_OF. A row with none of these reads complete, which says only that the fields are filled and the review is recent. Owner, risk tier and review date are our checks. NIST’s action asks for a documented context of use.

HR and people use cases get a separate mark. A use case is marked FOR LEGAL TO CHECK if its department is one of HR_DEPARTMENTS or its name or purpose holds one of HR_TERMS, in any case. The reason printed is that laws on automated employment decisions, such as NYC Local Law 144, Colorado SB 26-189 (we have not verified the bill text) and California’s automated decisionmaking technology rules under the CCPA, may apply, and that other laws may also apply. Local Law 144 restricts how employers use automated employment decision tools (NYC DCWP: Automated Employment Decision Tools (read 2026-10-31)), and the California Privacy Protection Agency lists adopted regulations on automated decisionmaking technology (California Privacy Protection Agency: regulations (read 2026-10-31)). The mark is a list for your lawyers. It is not a rating, and it does not say a use case is covered by any of these laws. Any term in HR_TERMS that matches no row is printed as information, so a word that matches nothing does not pass unseen.

    hr_terms = " OR ".join(f"contains({uc_text}, {sql_text(t)})" for t in terms) or "false"
    hr_depts = ", ".join(sql_text(d) for d in HR_DEPARTMENTS if d.strip()) or "NULL"
    gaps = " ".join(f"WHEN {k} IS NULL THEN '{label}'" for k, label in UC_REQUIRED)
    sql = f"""
        SELECT row_no, coalesce(use_case_id_shown, '-') AS use_case_id, last_reviewed,
               CASE {gaps}
                    WHEN review_text = '' THEN 'NO REVIEW DATE'
                    WHEN last_reviewed IS NULL THEN 'NOT WRITTEN YYYY-MM-DD: last reviewed'
                    WHEN last_reviewed < DATE '{REVIEW_AS_OF}' - {REVIEW_MAX_DAYS} THEN 'REVIEW OVERDUE'
                    ELSE 'complete' END AS status,
               CASE WHEN lower(trim(department)) IN ({hr_depts}) OR {hr_terms} THEN 'FOR LEGAL TO CHECK' ELSE '-' END AS legal
        FROM register ORDER BY row_no"""

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. A day with none before COMPLIANCE_API_ENABLED_ON is BEFORE ENABLED, because Anthropic does not backfill; any other day with none is NO RECORDS. 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)).

    day_sql = f"""SELECT d::DATE AS day, count(a.id) AS records,
                         CASE WHEN count(a.id) > 0 THEN 'recorded'
                              WHEN d::DATE < {en} THEN 'BEFORE ENABLED' ELSE 'NO RECORDS' END AS status
                  FROM generate_series(DATE '{p0}', DATE '{p1}', INTERVAL 1 DAY) t(d)
                  LEFT JOIN activities a ON a.ts::DATE = d::DATE GROUP BY 1 ORDER BY 1"""

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. 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.

Run 1: planted faults, and the script finds them

We ran the script on 2026-10-31 on generated records with faults planted. In the incident log: a row with no Reporter and no Impact(s), a Date of Incident written 2026/08/11, a System ID that is not on the register (UC-099), an incident dated 2026-09-10 and reported 2026-09-01, a row with no System ID, a Data Reported written 8/5/26, and a repeated row. In the register: four rows, one with no context of use, one with no owner, one with no intended purpose and one with no review date, a review last done on 2025-11-02, a date written 6/15/26, a repeated id, and a row with no id. One incident carries its System ID as  uc-001 , in lower case with a space before and after, and one register id as  uc-007 : neither is a fault, and the script should still match them. In the feed: one record duplicated, one timestamp unreadable, every record from 2026-08-10 to 2026-08-14 removed, and the Compliance API turned on on 2026-07-08, two days after the period began. The script prints RESULT: GAPS FOUND. It exits with code 0 unless you pass --strict, which exits 1 so a pipeline can stop.

Command: run 1
python3 nist_ai_rmf_claude_evidence.py --plant
Output from our run 1
input     : GENERATED records (not a real organization), seed 224, mode=plant
enabled on: 2026-07-08

== NIST documents (pinned: a different size or hash means NIST re-published the file)
  NIST AI 600-1 (Generative AI Profile, July 2024): 1174643 bytes, Last-Modified Mon, 24 Mar 2025 19:11:27 GMT, sha256 6e73620ab6b64e90ef2c04bf0e0d6246185a2f4b1b13cab0df494496cff89b6a
  NIST AI 100-1 (AI RMF 1.0, January 2023): 1946127 bytes, Last-Modified Wed, 04 Jun 2025 17:14:26 GMT, sha256 7576edb531d9848825814ee88e28b1795d3a84b435b4b797d3670eafdc4a89f1

== Input checks   (activity records read: 457; before the period: 0; after the period's end: 16, not used; incident log rows: 9; register rows: 9)
[ok  ] NIST documents that are not the pinned file: 0
[FAIL] duplicate activity ids: 1   (trips on: a duplicated record)
[FAIL] activity 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: days missing from the pull; 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] incident log rows repeated (same System ID, Title and Date of Incident): 1   (trips on: a duplicated incident row)
[FAIL] register ids listed more than once: 1   (trips on: a duplicated register row)
[FAIL] register rows with no use case id: 1   (trips on: a register row with no id)
[FAIL] incident log and register dates not written YYYY-MM-DD: 3   (trips on: a short date such as 8/5/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 activity ids 1; records with no id 0
[info] incidents with a Date of Incident inside the period: 8 of 9 (1 with no Date of Incident written YYYY-MM-DD, so not placed)

== GOVERN 4.3 / GV-4.3-002: incident_log_check   ** INCOMPLETE: see input checks **
period    : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters   : every row of the incident log, none dropped; row_no is the row's place in the file after the header; System ID matched to the register on the lower-cased, trimmed id; the nine fields as NIST's AI 600-1 lists them
run at    : 2026-10-31 16:45:21 UTC
rows      : 9
sha256    : e2c19afc3f4657655745285cb6c39f4f7681f0fb8f7af91c1b5442154a282a3e
  1 | UC-001 | 2026-07-18 | MISSING: Reporter, Impact(s)
  2 | UC-003 | - | NOT WRITTEN YYYY-MM-DD: Date of Incident
  3 | UC-002 | 2026-09-02 | complete
  4 | UC-099 | 2026-08-24 | SYSTEM NOT ON REGISTER
  5 | UC-006 | 2026-09-10 | INCIDENT DATED AFTER IT WAS REPORTED
  6 | - | 2026-09-14 | MISSING: System ID
  7 | UC-004 | 2026-08-04 | NOT WRITTEN YYYY-MM-DD: Data Reported
  8 | UC-002 | 2026-09-02 | complete
  9 | uc-001 | 2026-09-17 | complete

== MAP 1.1 / MP-1.1-002: use_case_register_check   ** INCOMPLETE: see input checks **
period    : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters   : every register row, none dropped; first applicable status; review overdue means last reviewed before 2026-09-27 minus 180 days; FOR LEGAL TO CHECK marks HR and people use cases by your HR_TERMS and HR_DEPARTMENTS
run at    : 2026-10-31 16:45:21 UTC
rows      : 9
sha256    : ab259e44b3f26fcf62bcd1b81963c429c641b9e158e7d4b8ddc806c0310e3757
  1 | UC-001 | - | NOT WRITTEN YYYY-MM-DD: last reviewed | -
  2 | UC-002 | 2026-07-02 | NO OWNER | -
  3 | UC-003 | 2026-08-20 | NO CONTEXT OF USE | -
  4 | UC-004 | 2025-11-02 | REVIEW OVERDUE | -
  5 | UC-005 | - | NO REVIEW DATE | FOR LEGAL TO CHECK
  6 | UC-006 | 2026-07-30 | NO INTENDED PURPOSE | -
  7 | uc-007 | 2026-08-15 | complete | FOR LEGAL TO CHECK
  8 | UC-003 | 2026-08-20 | complete | -
  9 | - | 2026-08-20 | NO USE CASE ID | -
  for Legal to check: 2 use case(s). Reason: laws on automated employment decisions, such as NYC Local Law 144, Colorado SB 26-189 (bill text not verified) and California's automated decisionmaking technology rules under the CCPA, may apply. Other laws may also apply. This is a list for Legal, not a rating.
[info] HR_TERMS that matched no register row: hiring, performance review, termination, employee monitoring

== GOVERN 1.5 / MANAGE 4.1: 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 16:45:21 UTC
rows      : 84
sha256    : 03965a69e5fff5548657218403867cd91412182a61283e48cd84bb4c33d90b8b
  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    : 20 item(s) need an explanation (incident rows and register rows with a gap, days with no records or before enablement)
RESULT: GAPS FOUND

Read the incident list first. Row 1 is missing two fields, so a reader sees which NIST fields the log does not hold for that incident. Rows 2 and 7 hold dates the script will not guess at. Row 4 names a use case the register does not know, so either the register is out of date, the incident was filed against the wrong system, or the id was mistyped. Row 9 is uc-001  and reads complete, which shows the matching held. Row 8 also reads complete but repeats row 3, which the input check counts. In the register list, the two rows marked FOR LEGAL TO CHECK are listed whatever their other status.

Run 2: clean records, and the script reports no gaps

The same script with no planted faults: three incident rows with all nine fields, six register rows with every field filled and a review inside 180 days, a feed with records on every day from the day the Compliance API was turned on. One register row, the resume screening use case, is still marked FOR LEGAL TO CHECK, because the mark is a list for Legal and not a gap. The generated feed also holds one duplicated id after the period’s end. It is not used, and the script prints it as information. The script prints RESULT: NO GAPS FOUND.

Command: run 2
python3 nist_ai_rmf_claude_evidence.py
Output from our run 2
input     : GENERATED records (not a real organization), seed 224, mode=clean
enabled on: 2026-06-01

== NIST documents (pinned: a different size or hash means NIST re-published the file)
  NIST AI 600-1 (Generative AI Profile, July 2024): 1174643 bytes, Last-Modified Mon, 24 Mar 2025 19:11:27 GMT, sha256 6e73620ab6b64e90ef2c04bf0e0d6246185a2f4b1b13cab0df494496cff89b6a
  NIST AI 100-1 (AI RMF 1.0, January 2023): 1946127 bytes, Last-Modified Wed, 04 Jun 2025 17:14:26 GMT, sha256 7576edb531d9848825814ee88e28b1795d3a84b435b4b797d3670eafdc4a89f1

== Input checks   (activity records read: 694; before the period: 199; after the period's end: 16, not used; incident log rows: 3; register rows: 6)
[ok  ] NIST documents that are not the pinned file: 0
[ok  ] duplicate activity ids: 0
[ok  ] activity 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  ] incident log rows repeated (same System ID, Title and Date of Incident): 0
[ok  ] register ids listed more than once: 0
[ok  ] register rows with no use case id: 0
[ok  ] incident log and register dates not written YYYY-MM-DD: 0
        earliest record: 2026-06-01; enabled on: 2026-06-01
[info] after the period's end, not checked: duplicate activity ids 1; records with no id 0
[info] incidents with a Date of Incident inside the period: 3 of 3 (0 with no Date of Incident written YYYY-MM-DD, so not placed)

== GOVERN 4.3 / GV-4.3-002: incident_log_check
period    : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters   : every row of the incident log, none dropped; row_no is the row's place in the file after the header; System ID matched to the register on the lower-cased, trimmed id; the nine fields as NIST's AI 600-1 lists them
run at    : 2026-10-31 16:45:27 UTC
rows      : 3
sha256    : d0e76435a95387a55368e26ebff452cc3eb92f861689db562fd296c18b376a81
  1 | UC-001 | 2026-07-18 | complete
  2 | UC-003 | 2026-08-11 | complete
  3 | UC-002 | 2026-09-02 | complete

== MAP 1.1 / MP-1.1-002: use_case_register_check
period    : 2026-07-06 to 2026-09-27 (UTC, inclusive)
filters   : every register row, none dropped; first applicable status; review overdue means last reviewed before 2026-09-27 minus 180 days; FOR LEGAL TO CHECK marks HR and people use cases by your HR_TERMS and HR_DEPARTMENTS
run at    : 2026-10-31 16:45:27 UTC
rows      : 6
sha256    : b9e339a468c693d39c642bde8faad2bd673d71014453bc4f1cb315653ed35b69
  1 | UC-001 | 2026-06-15 | complete | -
  2 | UC-002 | 2026-07-02 | complete | -
  3 | UC-003 | 2026-08-20 | complete | -
  4 | UC-004 | 2026-05-28 | complete | -
  5 | UC-005 | 2026-09-01 | complete | FOR LEGAL TO CHECK
  6 | UC-006 | 2026-07-30 | complete | -
  for Legal to check: 1 use case(s). Reason: laws on automated employment decisions, such as NYC Local Law 144, Colorado SB 26-189 (bill text not verified) and California's automated decisionmaking technology rules under the CCPA, may apply. Other laws may also apply. This is a list for Legal, not a rating.
[info] HR_TERMS that matched no register row: hiring, candidate, promotion, performance review, termination, employee monitoring

== GOVERN 1.5 / MANAGE 4.1: 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 16:45:27 UTC
rows      : 84
sha256    : 8b36c1c7e029fb6a7b1e70be1d3f9a3a94575bd3f9ace779ae243f783b8cd54d
  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 (incident rows and register rows with a gap, days with no records or before enablement)
RESULT: NO GAPS FOUND

No gaps found says the records are filled in, with readable dates. It does not say the incident log holds every incident, that a context of use is good, or that anyone acted on what the feed recorded.

Run 3: the enablement day not set

Run 2 again with COMPLIANCE_API_ENABLED_ON left empty (the script’s --enablement-unknown option). The feed does hold records, but without the day the Compliance API was turned on, nobody can tell whether the pull is complete. An unset setting fails the check instead of passing it. The lists are marked INCOMPLETE. Their incident and register rows read complete because the generated records are clean, so the INCOMPLETE heading is the warning, not the row status. Read the lists only after the input checks pass.

Command: run 3
python3 nist_ai_rmf_claude_evidence.py --enablement-unknown
Output from our run 3 (one line, and the result)
[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)
RESULT: GAPS FOUND

Run it on your own data

To run it on your own data, change only the settings block at the top of the file. Set ACTIVITIES_FILE, INCIDENTS_FILE and USE_CASES_FILE to the paths of your Activity Feed pull (one JSON object per line), your incident log and your use-case register (both CSV files with a header row). Set PERIOD_START, PERIOD_END and REVIEW_AS_OF to your period, COMPLIANCE_API_ENABLED_ON to the day you first turned the Compliance API on, and REVIEW_MAX_DAYS to your own review interval. Replace the column names in INC_COLS and UC_COLS with the names of the matching columns in your files; case does not matter. Add your own HR departments and HR words to HR_DEPARTMENTS and HR_TERMS, in any case. Write every date as YYYY-MM-DD.

We also ran the script on a second set of generated files with other file names, other column names in other case, a blank where an id could be, an HR department and an HR word of the reader’s own added to the settings, and faults planted for seven of the ten input checks. In the log it listed MISSING, NOT WRITTEN YYYY-MM-DD, SYSTEM NOT ON REGISTER and INCIDENT DATED AFTER IT WAS REPORTED. In the register it listed NO CONTEXT OF USE, NO OWNER, NO REVIEW DATE, REVIEW OVERDUE and NO USE CASE ID. It matched ab-1 in the log to AB-1 in the register, failed the input checks for the duplicated activity id, the unreadable timestamp, the two days with no records, the repeated incident row, the repeated register id, the register row with no id and the date written 15/07/2026. It marked FOR LEGAL TO CHECK a department written  people ops  and a purpose holding “manager’s review”, both added to the settings in another case, and printed RESULT: GAPS FOUND. We have not run it on a real pull.

Download nist_ai_rmf_claude_evidence.py
Download the full guide (PDF)

Method

  • Input: generated Activity Feed records, written by the script as JSON lines in the Activity object’s shape, with a fixed seed (224), plus a generated incident log and a generated use-case register. Fields read from the feed: id, created_at, type (Anthropic: Query the Activity Feed (read 2026-10-31)).
  • NIST text: the field names and the action texts are read from NIST AI 600-1 (read 2026-10-31), and the Core text from NIST: AI RMF Core (read 2026-10-31). The script pins NIST AI 600-1 and NIST AI 100-1 by size and SHA-256. The Core web page is not pinned.
  • Period: 2026-07-06 to 2026-09-27, UTC, inclusive. Days are counted by the created_at date in UTC. Records after the period’s end are not used; a duplicate or a record with no id there is printed as information.
  • Settings you can disagree with are at the top of the script: the review interval, which columns hold which field, and which words mark an HR use case.
  • Every problem row is counted. An unreadable date is listed with a status, never dropped, and counted by an input check. A failed input check marks the lists INCOMPLETE.

Limits

  • This is not a client result. The records are generated. The runs show the script finds planted faults. They say 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.
  • Following NIST is not something a script can show. The framework is voluntary and gives no pass mark. The script checks that the incident log holds the nine example fields NIST lists, that each register row states a context of use, and that the feed recorded each day. Owner, risk tier and review date are our checks.
  • The Activity Feed fields the script reads carry no incident report and no use-case id. Both lists depend on records your company keeps, and the script cannot tell whether the incident log or the register is complete.
  • 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.
  • A recorded day is not a reviewed day. GOVERN 1.5 and MANAGE 4.1 ask for monitoring that is planned and carried out. The script shows the record exists, not that anyone read it.
  • The HR mark is a list for Legal. Whether any of these laws applies to a use case is a question for your lawyers.
  • Our reading is not NIST’s. The mapping of ids to lists is ours. NIST has not reviewed it.
  • The generated incidents and use cases are invented. Names, ids and addresses in them belong to no one.
  • Your incident log may hold workforce emails and sensitive detail, 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. It does fetch the two NIST PDFs.

Tested on

Tested on

Platform
DuckDB 1.5.6, Python 3.14, run from Python with the duckdb module
Edition
Open-source DuckDB, in-memory
Date
2026-10-31
Dataset
Generated records, seed 224 (not a real organization). Feed fields from Anthropic’s Activity object (read 2026-10-31); field names and action texts from NIST AI 600-1 (read 2026-10-31)
Code
nist_ai_rmf_claude_evidence.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 settings 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.
  • NIST asks for more than two records. Your policies, your measurement of risk, your roles and what you did about each finding sit in your own records.
  • It is a judgment, not a score. Whether what you can show is enough is for your own risk owners and counsel.

See Control Tower for Claude

Control Tower for Claude runs in your own Azure subscription.

Open the Control Tower for Claude page