# Reading a Salesforce org without changing it

Read-only routes, SOQL starters and the commands never to run. Checked in a browser on 2026-11-06. developer.salesforce.com refuses automated fetches. help.salesforce.com answers, but some pages return an error shell with no article, so read those in a browser. Confirm field names with `sf sobject describe` (add `--use-tooling-api` for Tooling API objects) before running a query.

## Baseline first

- Prefer a full or partial sandbox refreshed from production for metadata-heavy reads, and record which environment you read and when it was refreshed. Counts that matter for findings (owners, open opportunities, duplicates) come from production aggregate queries.
- Record the running user's profile and permission sets: `sf org list` for the username, then `SELECT Id, Profile.Name FROM User WHERE Username = '<username>'` and `SELECT PermissionSet.Name FROM PermissionSetAssignment WHERE AssigneeId = '<user id>'`. A user with Modify All Data can write anywhere; that is a reason to deny write tools, not a reason to proceed carelessly.
- If the identity can write, list every write tool by name and ask the user to set each to ask or deny in the client. Wait for their confirmation before the first read, and record the denied tools in the baseline.
- Stop if the org you can read is not the one the user named (another business unit, an old sandbox).

## 1. Salesforce CLI (sf), read commands only

The user authenticates in their own terminal. The agent never sees a token.

- `sf org list` to confirm the alias and whether it is a sandbox.
- `sf org list limits --target-org <alias>` for `DailyApiRequests` (INT-04).
- `sf sobject describe --sobject Opportunity --target-org <alias> --json > describe-opportunity.json`, one file per object, then `normalize_exports.py salesforce-describe`.
- `sf data query --query "<SOQL>" --target-org <alias>`; add `--use-tooling-api` for the Tooling API queries in section 2; add `--result-format csv` to save results for the scripts.
- Never run `sf org display` in any form (it can print an access token), any `sf data create`, `update`, `delete`, `upsert` or `import`, `sf project deploy`, `sf apex run`, or permission set assignment commands.

## 2. SOQL starters (aggregate counts and configuration only)

Confirm each field with describe before running. Keep `CreatedBy` and user names out of the shared report; map user IDs to role labels.

```sql
SELECT Id, UserType, Profile.Name, LastLoginDate FROM User WHERE IsActive = true
SELECT AssigneeId, PermissionSet.Name, PermissionSet.IsOwnedByProfile FROM PermissionSetAssignment
  WHERE (PermissionSet.PermissionsModifyAllData = true OR PermissionSet.PermissionsViewAllData = true) AND Assignee.IsActive = true
SELECT COUNT() FROM Opportunity WHERE IsClosed = false AND Owner.IsActive = false
SELECT COUNT() FROM Opportunity WHERE IsClosed = false AND AccountId = null
SELECT MasterLabel, IsClosed, IsWon, DefaultProbability, ForecastCategoryName, SortOrder FROM OpportunityStage WHERE IsActive = true
SELECT MasterLabel, IsConverted, IsDefault FROM LeadStatus
SELECT ApiName, Label, Description, ProcessType, TriggerType, RecordTriggerType, TriggerObjectOrEventLabel, TriggerOrder, IsActive, LastModifiedDate FROM FlowDefinitionView
SELECT DeveloperName, SobjectType, IsActive FROM DuplicateRule
SELECT Action, Section, CreatedDate FROM SetupAuditTrail WHERE CreatedDate = LAST_N_DAYS:90
SELECT AppName, LastUsedDate, UseCount FROM OauthToken
SELECT AccountId, Field, DataType, OldValue, NewValue, CreatedDate, CreatedById FROM AccountHistory
  WHERE Field = 'Type' AND CreatedDate = LAST_N_DAYS:30
```

Tooling API (`--use-tooling-api`):

```sql
SELECT ValidationName, Active, EntityDefinition.QualifiedApiName FROM ValidationRule
SELECT Name, TableEnumOrId FROM WorkflowRule
SELECT QualifiedApiName FROM FieldDefinition
  WHERE EntityDefinition.QualifiedApiName = 'Account' AND IsFieldHistoryTracked = true
SELECT QualifiedApiName, Description FROM FieldDefinition
  WHERE EntityDefinition.QualifiedApiName = '<Object>'
SELECT ActiveVersionId FROM FlowDefinition WHERE DeveloperName = '<ApiName>'
SELECT Metadata FROM Flow WHERE Id = '<active version id>'
```

- Check in Setup which validation and workflow rules are active; `WorkflowRule` has no active flag.
- Two starters are not on Salesforce's object pages (checked 2026-11-06), so confirm both with describe: `LastModifiedDate` on FlowDefinitionView, and the `EntityDefinition.QualifiedApiName` path on ValidationRule, whose documented field is `EntityDefinitionId`.
- `OauthToken` covers connected apps only, shows only your own tokens without Customize Application, and stops at 2,500 rows, so run a COUNT first and split larger results. `DuplicateRule` needs View Setup and Configuration.
- Run the `IsFieldHistoryTracked` query once per object that has critical fields. Salesforce says true means the field's history can be tracked, which is looser than tracking being on, so check one object against Field History Tracking in Setup. A critical field that is not tracked has no history, so its FLD-02 history evidence is NOT CHECKED (`critical_fields[].history_tracked` false), and the first change request turns tracking on. Tracking covers up to 20 fields per object and starts when it is turned on. History is kept up to 18 months, or 24 through the API, unless Field Audit Trail is on.
- Run the `Description` query once per object in scope and save each result as CSV named after its object (`fielddefinition-opportunity.csv`). Pass each with `normalize_exports.py salesforce-describe --descriptions`, because describe output has no descriptions; without them FLD-06 is NOT CHECKED.
- Save the `FlowDefinitionView` result as CSV, keeping `TriggerOrder` and `Description`, and run `normalize_exports.py salesforce-flows`. Without `TriggerOrder`, AUTO-09 warns about every object with two or more record-triggered flows of one timing, even where an order is set; the script prints a note when the column is missing.
- `FlowDefinitionView` cannot show field writes, actions or filters. Read every active flow's metadata, with the `FlowDefinition` and `Flow` queries (`Metadata` can only be queried one record at a time, and flows in managed packages return it null unless they are templates) or one Org Check run, then add the writes, actions and filter references per flow in the inventory and set `writes_known`, `actions_known` and `refs_known` to true. While any active flow is unread, LIFE-05, AUTO-03, AUTO-04, AUTO-05, AUTO-06, AUTO-07 and AUTO-10 cannot pass and name the unread flows, and the automation verdict reads Not assessable. That is the honest result for an org read in part.
- Field history queries feed `writer_history.py --format salesforce` with `--writer-map` (user ID to label, such as `integration:billing-middleware` or `user:sales-rep`). Unmapped user IDs count as automated writers, so map every ID before rating FLD-02. Process Builder and record-triggered flows run in system context without sharing by default. Their running user is the person whose edit launched them, so expect history to show that person. Salesforce skips the history row when that user can't edit the object, so a missing row doesn't prove nothing changed.
- Lookup fields such as Owner write an ID row and a name row per change; keep `DataType = 'EntityId'` rows only. `writer_history.py` drops the name rows when `DataType` is in the export, and merges rows that share record, field, `CreatedDate` and `CreatedById` when it is not; otherwise one change counts twice and a flip-flop disappears. Salesforce's history page does not document this two-row pattern (checked 2026-11-06), so confirm it on one owner change.

## 3. Hosted MCP server, SObject Reads only

- Hosted MCP servers are off by default. An admin enables one in Setup (API Catalog > MCP Servers), which can take up to 2 minutes, with an External Client App that has the `mcp_api` and `refresh_token` scopes; Connected Apps are not supported. Production and sandbox endpoints differ, and orgs that block logins from login.salesforce.com or test.salesforce.com use a My Domain URL.
- Use only the SObject Reads server: schema discovery, SOQL queries, search and related-record retrieval, as the signed-in user under object, field and sharing rules. No data changes are possible through it.
- Never connect the Mutations, Deletes or All servers for an audit.
- The Salesforce DX local MCP server (`@salesforce/mcp`) reached end of life on 2 November 2026 and its repositories are archived. Do not use it, and raise INT-05 if it is still configured.
- Salesforce says any MCP server will need agent registration within three months of its notice, with the new controls targeted for November 2026. Check the current state before connecting.

## 4. Org Check (Salesforce Labs, MIT)

- Run the local sf plugin `@orgcheck/sfdx-plugin` against a sandbox, by an admin, for example `sf check global-view --target-org <alias> --json-file orgcheck.json`. Confirm the command with `--help`; it is not an officially supported product.
- It reads metadata only but expects a user with Modify All Data, Author Apex, API Enabled and Install Packaging. It consumes daily API requests, warns at 70% and stops at 90% of the allocation, so run it off-peak.
- Do not install the AppExchange package in production as part of the audit.
- Its output feeds AUTO evidence (flow anti-patterns through the bundled Lightning Flow Scanner, flows per object and their dependencies), FLD evidence (unused fields) and ACC evidence (permission anti-patterns).

## 5. UI evidence (captures, no exports of people)

- User Access Summary: Setup > Users > the user > View Summary, then Field Permissions and Access Granted By. Use it for every integration user (ACC-02, FM-17).
- Sharing Settings: the organization-wide defaults per object and the sharing rules for Account, Opportunity and any object holding restricted records (ACC-05). Opportunity at Public Read/Write opens every opportunity, and its amount wherever field-level security allows, to every internal user with access to the object.
- Field Access Summary: Object Manager > the object > Field Access, per restricted field (ACC-05).
- Flow Trigger Explorer per object for record-triggered flows and their order (AUTO-09).
- System Overview for API usage over the last 24 hours; Health Check score for the security owner.
- Connected Apps OAuth Usage covers connected apps only, and leaves out username-password flow apps. External Client App usage, which includes hosted MCP connections, comes from the Connect REST API.
- Lead conversion field mapping, duplicate and matching rules, report type relationships for headline reports.

## 6. Behaviours that shape findings

- Muting applies only inside its permission set group; the same permission from a profile or another set still applies.
- View All Fields grants broad field visibility; View All Data and Modify All Data do not by themselves override field-level security.
- Formula and roll-up fields can be visible while referencing fields the user cannot see.
- Page layouts organise the interface; they are not security and do not stop API writers.
- Apex compiled at API 67.0 or later defaults to user mode for database operations; 66.0 and earlier default to system mode.
- Asynchronous flow paths are not guaranteed to run in the displayed order.
- Daily API requests are measured over a rolling 24 hours and shared by every integration. The REST Limits resource needs View Setup and Configuration and is accurate within about five minutes. Production orgs and sandboxes allow 25 concurrent long-running requests (20 seconds or more); Developer Edition and trial orgs allow 5.
- Salesforce ended support for Workflow Rules and Process Builder on 31 December 2025. Active ones keep running with no support or bug fixes.
- Smart Totaling can count a repeated parent amount once in a report summary while a flat export adds it several times. A report type "with" a child object drops parents without one.
- 15-character IDs are case-sensitive and 18-character IDs are not; joining the two forms breaks matches. Normalise to 18 characters before comparing exports.
- Merging accounts keeps the master and deletes the others; downstream systems holding the deleted IDs need a remap.
- Upsert by external ID creates a record when nothing matches unless the integration uses `updateOnly`.
