30-second answer

migration-read records what a drive holds, at a moment you name, in a document that can be compared against another one later. It is the half of migration verification that has to happen before the move: a migration cannot be verified after the fact, because decommissioning the source is the point of the exercise and a record produced at sign-off has nothing left to compare against.

PowerShell
m365-governance migration-read --drive <id> --read-id <name> `
  --taken-at 2026-07-04T06:10:22Z --estate "contoso-projects" `
  --out baseline.json

What it needs, and what it never does

The command spends a token; it never acquires one.

Text
M365_GOVERNANCE_GRAPH_TOKEN is not set. This command spends a token somebody
already holds; it never acquires one.

That is deliberate. A collector that logs itself in decides which identity read the estate, and the identity that read it is exactly what a reader needs to weigh a missing item: what one account cannot see is absent rather than refused. The read records what the token says about itself, never the token: the kind of identity (application, delegated, or not-established when the token carries neither claim), the permissions it was granted and the tenant. It does not record which account it was, so two people holding the same permissions produce reads that cannot be told apart.

--taken-at is supplied rather than read from the clock. The moment a read was taken decides which side of a move it is, so it belongs to whoever ran the collection, not to whichever machine happened to serialise it.

Options

--drive is the drive to enumerate, and --folder an item id to start at inside it.

--read-id is required: the id this read is recorded under. migration-verify names the baseline and the verification read by it.

--estate is what was read, as you name it. --out writes the read there.

The two flags that cost

FlagWhat it addsWhat it costs
--with-versionshow many versions each file carriesone extra request per file
--with-permissionsgrants and sharing links per fileone extra request per file; Graph cannot expand permissions on a collection

Without them the read carries no versions or permissions, and a verification built from it reports those dimensions as not compared, because a read did not carry them. That is a different reason from a source that cannot provide them at all, and the record keeps the two apart: a dimension nobody asked for is not a dimension that failed. The read itself does not record whether the flags were given.

What it writes

A migration-read/1.0.0 document. This one is the baseline the engine's own end-to-end test produces at 1.0.0b9, from simulated Graph answers; no tenant was read:

JSON
{
  "$schema": "https://ph7x.com/schemas/m365-governance/migration-read/1.0.0",
  "read_id": "baseline-001",
  "taken_at": "2026-07-04T06:10:22Z",
  "estate": "contoso-projects",
  "produced_by": "graph-migration-read via Microsoft Graph v1.0",
  "coverage": [],
  "items": {
    "/plan.xlsx": { "size": 4096, "author": "a.pereira@example.test",
                    "content_digest": "AAAA" },
    "/index.js":  { "size": 512,  "author": "a.pereira@example.test",
                    "content_digest": "BBBB" }
  },
  "read_by": { "kind": "not-established", "scopes": [], "tenant": "t" },
  "content_digest_algorithm": "quickXorHash"
}

read_by says not-established because the test's token carries neither roles nor scp; a real token yields application or delegated, with the permissions it was granted.

coverage is the most important field in the document and the one most reports do not have. It carries what this read could not establish, and why: a folder the account was refused, a walk that stopped at a depth, an item listed by the enumeration and gone when it was read. A verification that cannot say what it failed to read is indistinguishable from one that read everything, and the two are the opposite of each other.

What it will not walk

An item shared from another drive is listed and never entered. Walking into it would put another estate's items under this estate's identities, and every one of them would then read as present or missing here on the strength of a read that never covered that drive. The record says so, per item:

JSON
{ "scope": "/Shared/other-drive-folder", "state": "not-supported",
  "detail": "this item is shared from another drive; its contents are not
             part of this estate and were not walked" }

What it costs at scale

The walk holds the whole estate in memory, which is what makes it possible to canonicalise and digest the result in one pass. Measured on 1 November 2026 at 1.0.0b8, on one Apple silicon machine with Python 3.14, over a synthetic estate of files in folders of 200:

ItemsTimePeak memory of the read
12,0000.02 seconds5 MB
250,0000.6 seconds103 MB

Roughly 410 bytes an item, linear. Comparing two reads of 250,000 items took under two seconds and about 45 MB on top of the two reads. The times exclude the network entirely: the Graph answers were simulated. A real read makes one request per page of each folder's children, plus one per file for each of --with-versions and --with-permissions, and no figure here measures those. The engine's own test holds the read under 2,048 bytes an item; it does not time it.

Exit codes

0 the read was taken; it is written to a file only with --out. A library that answers with nothing in it, and refuses nothing, is an empty read and also 0. 1 Graph refused the request, or nothing could be enumerated and part of the estate was refused, which is refused rather than returned as an empty estate. 2 the token was not set.