v0.3.0 AI Drafted

Miro Hardening Guide

Productivity Last updated: 2026-09-25

Visual collaboration security for board sharing, app governance, classification guardrails, and AI controls

View:

Overview

Miro is a visual collaboration platform for whiteboards, diagrams, and design sessions. REST API, OAuth integrations, and public board sharing handle sensitive planning documents and architecture diagrams. Compromised access exposes strategic planning, product roadmaps, and internal processes.

Intended Audience

  • Security engineers managing collaboration tools
  • Miro team administrators
  • GRC professionals assessing visual collaboration security
  • Third-party risk managers evaluating design tool integrations

How to Use This Guide

  • L1 (Crawl): Essential controls for all organizations
  • L2 (Walk): Enhanced controls for security-sensitive environments
  • L3 (Run): Strictest controls for regulated industries

Scope

This guide covers Miro security configurations including authentication, access controls, and integration security.

Automation surface: Miro publishes no Terraform provider and no first-party CLI, and has no vendor-native configuration files. Its official SDKs, @mirohq/miro-api for Node.js and miro_api for Python, are clients for the same REST API, so the api/ packs below cover that surface too. The REST API reads most of the settings in this guide, but only on the Enterprise plan with a Company Admin token, and it has no endpoint for SSO, 2FA, idle session timeout, SCIM enablement, app governance, or Miro AI capability settings. Controls with a read endpoint carry a read-only audit pack. The rest carry an Automation: ClickOps-only line. For SIEM, Miro documents connectors that read the same audit-log API: an Enterprise SIEM access token (Admin console → Apps and integrations → Enterprise integrations) for its IBM QRadar and Splunk apps, and a Microsoft Sentinel connector. No detection-rule pack (siem/ or Sigma) ships, because the audit-log reference types event as a free string with one example value (sign_in_succeeded), so a rule keyed on event names could not be checked against Miro’s documentation. The 4.1 pack exports events as NDJSON for any SIEM, and 4.1’s Detection Focus lists what to alert on. Sources: Miro REST API reference, Miro Python client, Miro Node.js client, Setting up SIEM, Microsoft Sentinel integration, audit logs API reference (all checked 2026-09-24).


Table of Contents

  1. Authentication & Access Controls
  2. Board & Content Security
  3. Integration Security
  4. Monitoring & Detection

1. Authentication & Access Controls

1.1 Enforce SSO with MFA

Profile Level: L1 (Crawl) NIST 800-53: IA-2(1)

Description

Require SAML SSO with enforced multi-factor authentication for all Miro access, or 2FA where SSO is unavailable, so every login is brokered through the corporate identity provider. SAML SSO is available on both the Business and Enterprise plans and is configured by a Company Admin.

Rationale

Why This Matters:

  • Centralizes Miro authentication in the corporate IdP, enforcing MFA and conditional access on every login
  • Magic-link and social-account sign-ins sit outside the IdP’s MFA and conditional-access policy, so a compromised mailbox or personal Google/Microsoft/Apple account becomes a Miro login
  • Enforced SSO lets you deprovision departed users centrally, eliminating orphaned accounts with standing board access
  • Boards hold architecture diagrams, roadmaps, and strategic planning — a single compromised login can expose all of it

Attack Prevented: Credential theft, phishing, MFA bypass, password reuse, orphaned-account access

Enabling SSO removes the alternate login paths. Miro documents that once SSO is enabled, “Other login options will be disabled for users, including magic link, Google, Facebook, Slack, AppleID and O365.” Existing sessions keep working until they expire or the user logs out, so plan the cutover around session expiry rather than expecting an instant change. Source: Single sign-on (SSO) (content verified via the vendor’s help-center API, 2026-09).

Who is actually forced through SSO: SSO login is required for users on domains listed in your SSO settings. In an Enterprise organization, users on verified domains become managed users who must authenticate with SSO; for users on unverified domains in the same organization, “a magic link or social account is used for authentication.” Miro no longer offers password sign-in at all (“We have removed the option to sign in with username and password”, Two-factor authentication (2FA)), so the non-SSO paths are magic link and social accounts. Verify every domain you intend to govern, and use domain control (Enterprise) to restrict which teams those managed users can reach.

ClickOps Implementation

Step 1: Configure SAML SSO (Business and Enterprise)

  1. Verify each domain whose users must be governed by SSO. SSO login only becomes available after your domains are verified
  2. Configure the Miro application in your IdP using Miro’s SP metadata
  3. Navigate to: Company settings → Security → Single sign-on (Business) or Company settings → Security & Compliance → Authentication → Single sign-on (Enterprise)
  4. Toggle on: SSO/SAML
  5. Enter the SAML Sign-in URL and Public Key x.509 Certificate from your IdP, then under Users from these domains will sign in using SSO select your verified domains
  6. Before cutover, keep a break-glass admin whose email domain is not in the SSO domain list. This is Miro’s own lockout guidance; otherwise only Miro support can disable SSO for the organization

Step 2: Additional Identity Providers (Enterprise, private beta)

  1. Multiple IdPs per organization are a private beta. Miro’s SSO article asks Enterprise organizations to sign up for it; Business plans support a single IdP

Step 3: Enforce 2FA for Sign-Ins That Bypass SSO

  1. Enterprise: navigate to Admin console → Security → Authentication and toggle on Enforce 2FA for non-SSO users
  2. Business: navigate to Admin Console → Security → Authentication, then under Two-factor authentication (2FA) toggle on Require two-factor authentication when signing in
  3. Starter: navigate to Admin Console → Security → Permissions and toggle on the same setting
  4. 2FA covers magic-link sign-ins only. Miro states that “2FA doesn’t apply when signing in with SSO or a social account (Google, Microsoft, Apple, etc.)”, so it does not replace SSO enforcement for social-account users

Sources: Single sign-on (SSO), Enterprise two-factor authentication (2FA) – admin guide, Two-factor authentication (2FA) (content verified via the vendor’s help-center API, 2026-09).

Automation: ClickOps only — Miro exposes no write interface for this setting (Miro REST API reference, 2026-09-24). The reference has no SSO, SAML or 2FA endpoint.


1.2 Team Access Controls

Profile Level: L1 (Crawl) NIST 800-53: AC-3, AC-6

Description

Define least-privilege team roles (Admin, Member, Guest) and configure member permissions and guest access policies so users receive only the board access their function requires.

Rationale

Why This Matters:

  • Role separation limits how much any single account can do, containing the blast radius if it is compromised
  • Unrestricted Admin or Member rights let any user reshare or export sensitive boards
  • Scoping guests to specific boards confines external collaborators instead of exposing the whole team space
  • Periodic review of roles catches privilege creep and stale guest access that should have been revoked

Attack Prevented: Privilege escalation, lateral movement, excessive access, insider data exposure

ClickOps Implementation

Step 1: Define Team Roles

Role Permissions
Admin Full team management
Member Create/edit boards
Guest Board-specific access

Step 2: Restrict Invitations and Guests

  1. Navigate to: Admin Console → Teams → {team} → Settings (Business and Enterprise, Company Admin) or Admin Console → Security → Permissions (Free, Starter and Education, Team Admin) (Team invitation settings, Team permissions on Enterprise plan)
  2. Under Invitation, limit who can invite users to the team to admins
  3. (Business and Enterprise) Set guests to Don’t allow unless the team works with outside collaborators
  4. (Free, Starter and Education) Leave Enable team invite link off, because anyone who has the link can join the team

Code Implementation

Code Pack: API Script
hth-miro-1.02-audit-team-roles.sh View source on GitHub ↗
# Per team: role counts from the members listing (paginated), plus who may
# invite and whether non-team collaborators are allowed, from team settings.
audit_team() {
  local tid="$1" cursor="" prev="" pages=0 q admins members guests unknown who ext issues=""
  : > "${ROLES_FILE}"
  while :; do
    q="limit=100"
    [ -z "${cursor}" ] || q="${q}&cursor=$(jq -rn --arg c "${cursor}" '$c|@uri')"
    api_get "/v2/orgs/{org_id}/teams/{team_id}/members" "/v2/orgs/${ORG_ID}/teams/${tid}/members?${q}"
    printf '%s' "${BODY}" | jq -e '.data | type == "array"' >/dev/null 2>&1 || precondition "member listing has no data array"
    printf '%s' "${BODY}" | jq -r '.data[] | (.role // "unknown")' >> "${ROLES_FILE}"
    prev="${cursor}"
    cursor=$(printf '%s' "${BODY}" | jq -r '.cursor // ""')
    pages=$((pages + 1))
    [ -n "${cursor}" ] || break
    [ "${cursor}" != "${prev}" ] || precondition "member listing repeated its cursor — refusing a partial result"
    [ "${pages}" -lt 1000 ] || precondition "member listing exceeded 1000 pages — refusing a partial result"
  done
  count_roles admins  -x 'admin'
  count_roles members -x 'member'
  count_roles guests  -x 'non_team'
  count_roles unknown -vxE 'admin|member|non_team'
  TOTAL_SEEN=$((TOTAL_SEEN + admins + members + guests + unknown))

  api_get "/v2/orgs/{org_id}/teams/{team_id}/settings" "/v2/orgs/${ORG_ID}/teams/${tid}/settings"
  who=$(printf '%s' "${BODY}" | jq -r '.teamInvitationSettings.whoCanInvite // "unknown"')
  ext=$(printf '%s' "${BODY}" | jq -r '.teamInvitationSettings.inviteExternalUsers // "unknown"')

  [ "${admins}" -le "${MIRO_MAX_TEAM_ADMINS}" ] || issues="${issues} admins>${MIRO_MAX_TEAM_ADMINS}"
  case "${who}" in
    only_org_admins|admins) ;;
    all_members) issues="${issues} every-member-can-invite" ;;
    *)           issues="${issues} whoCanInvite-unreadable(${who})" ;;
  esac
  [ "${unknown}" -eq 0 ] || issues="${issues} ${unknown}-unrecognised-roles"

  printf '  team …%s  admins=%s members=%s non_team=%s whoCanInvite=%s inviteExternalUsers=%s  %s\n' \
    "${tid: -6}" "${admins}" "${members}" "${guests}" "${who}" "${ext}" \
    "$([ -z "${issues}" ] && echo OK || echo "FINDING:${issues}")"
  [ -z "${issues}" ] || FINDINGS=$((FINDINGS + 1))
}

The pack reads each team’s role counts and its invitation policy (whoCanInvite, inviteExternalUsers). It needs an Enterprise organization and a Company Admin token with organizations:teams:read. On other plans the endpoints return “Invalid access” and the review stays in the console.


1.3 Enforce Idle Session Timeout

Profile Level: L2 (Walk) NIST 800-53: AC-11, AC-12

Description

Enable Miro’s Idle Session Timeout so inactive members and guests are automatically logged out of their Miro profile and must re-authenticate — through SSO where SSO is enforced — before reaching Enterprise board data. Enterprise plan, Company Admin.

Rationale

Why This Matters:

  • Miro sessions persist indefinitely by default; an abandoned tab on an unlocked or shared device stays fully authenticated to every board the user can reach
  • Automatic logoff bounds the window in which a stolen session cookie or a walk-up attacker can act
  • The timeout applies to guests as well as members, closing the gap where external collaborators hold the longest-lived sessions
  • Combined with enforced SSO, timeout forces re-authorization through the IdP, so revoked IdP access actually takes effect on live sessions

Attack Prevented: Session hijacking, unattended-device access, stolen-cookie replay, lingering access after IdP revocation

ClickOps Implementation

Step 1: Enable the Timeout

  1. Navigate to: Company settings → Security → Authentication → Idle Session Timeout
  2. Toggle on: Automatically log out inactive users
  3. Set the Timeout limit

Step 2: Choose a Defensible Duration

  1. First activation populates a 1-day default; the allowed range is 1 hour to 14 days (custom integer 1–9999 with minutes/hours/days units)
  2. Miro recommends no less than 8 hours — shorter values log users out mid-board and drive workarounds
  3. Note that inactivity means no mouse movement, clicks, or keystrokes anywhere in the app; users get a warning several minutes before logout
  4. Where a user belongs to multiple organizations with different intervals, the shortest duration wins

Automation: ClickOps only — Miro exposes no write interface for this setting (Miro REST API reference, 2026-09-24). The reference has no idle-session or timeout endpoint.

Validation & Testing

Sign in as a test member, leave the session untouched past the configured limit, and confirm the warning appears and the session terminates. Source: Idle Session Timeout (content verified via the vendor’s help-center API, 2026-08).


1.4 Automate Provisioning and Deprovisioning with SCIM

Profile Level: L2 (Walk) NIST 800-53: AC-2, AC-2(1)

Description

Configure SCIM between your identity provider and Miro so accounts and group membership are created, updated, and deactivated automatically from the directory of record. Enterprise plan, set up by Company Admins; SAML SSO must already be working before SCIM is configured.

Rationale

Why This Matters:

  • Manual offboarding leaves orphaned Miro accounts with standing access to boards long after the user left the organization
  • SCIM-driven deactivation removes access at the same moment the IdP does, closing the gap between HR offboarding and tool offboarding
  • Group synchronization keeps Miro user groups aligned with directory structure, preventing the privilege drift that manual membership edits create
  • Miro’s newer model maps a SCIM group to a Miro user group (not a team), so a single synced group can carry board and Space sharing across multiple teams

Attack Prevented: Orphaned-account access, privilege creep, delayed offboarding, unmanaged membership sprawl

Prerequisites

  • Enterprise plan
  • SAML SSO configured and functional
  • Company Admin role

ClickOps Implementation

Step 1: Enable SCIM

  1. Confirm SAML SSO is configured and working
  2. Navigate to: Company settings → Enterprise integrations, enable SCIM Provisioning, and copy the Base URL and API Token for your IdP (SCIM)
  3. To rotate the token later, click Generate new token in the SCIM Provisioning section, then Generate, and enter the new token in your IdP
  4. Migrate to the user-group synchronization model if you are still on the legacy team-mapped model

Step 2: Configure the IdP

  1. Add the Miro SCIM application in your IdP (Okta and Entra ID have first-party guides)
  2. Enable provisioning, updates, and deactivation — deactivation is the control that matters here
  3. Optionally link IdP groups to Miro user groups for sharing and @mentions

Automation: ClickOps only — Miro exposes no write interface for this setting (SCIM, Miro REST API reference, 2026-09-24). SCIM is enabled and its token generated in the console. Miro’s SCIM API is the protocol your IdP calls to provision users. It does not switch SCIM on.

Validation & Testing

Deactivate a test user in the IdP and confirm the corresponding Miro account loses access. Note Miro’s SCIM email-change guardrails: updates are rejected with a 400 error when the current or target domain is claimed by a different organization. Source: SCIM (content verified via the vendor’s help-center API, 2026-08).


2. Board & Content Security

2.1 Configure Sharing Defaults

Profile Level: L1 (Crawl) NIST 800-53: AC-21

Description

Control board sharing to prevent data exposure.

Rationale

Attack Scenario: Public boards containing architecture diagrams indexed by search engines; competitive intelligence exposed.

Why This Matters:

  • Public and “anyone with the link” boards are reachable without authentication and can be indexed by search engines
  • Default-open sharing leaks sensitive diagrams the moment a board is created, before anyone reviews permissions
  • Domain restrictions keep boards inside the organization and block accidental external sharing
  • Disabling public boards forces every share decision through an authenticated, auditable path

Attack Prevented: Unauthenticated data exposure, search-engine indexing of internal diagrams, competitive intelligence leakage, accidental oversharing

Plan note: the organization-wide sharing policy below is Enterprise-only, set by a Company Admin. Below Enterprise, public sharing is a per-board choice (Share → Anyone with the link) that board owners make and undo.

New teams start open. Miro documents that “Sharing via a public link is turned on by default on Team level and set to ‘Anyone can view and comment’ for newly created teams.” If the Company-level toggle below is off, teams cannot share publicly whatever their own setting says.

ClickOps Implementation

Step 1: Disable Public Sharing (Enterprise, Company Admin)

  1. Navigate to: Company settings → Security → Sharing
  2. Toggle off: Boards can be shared publicly
  3. This removes the Anyone with the link option from every board’s Share menu. Boards already shared by public link or embedded on sites become unavailable to public users, and their active sessions close. If public sharing is re-enabled later, each board has to be re-shared by hand
  4. Per team: go to Teams, select the team, open Settings, scroll to Sharing settings, and under Board sharing → By public link restrict public sharing

Step 2: Restrict Sharing to Allowed Domains (Enterprise, Company Admin)

  1. Navigate to: Company settings → Security → Sharing
  2. Toggle on: Restrict allowed domains, and add the domains your organization uses
  3. Leave Allow sharing with guests outside these domains unchecked unless guests are a deliberate business need
  4. Use Verify all users against the allowlist to find existing users whose domain is not allowed
  5. A team-level allowlist (Teams → {team} → Settings → Allowed domains for team) overrides the company list for that team, so review team lists too

Step 3: If Public Links Must Stay On

  1. In Company settings → Security → Sharing → Content, select Expire public sharing link and set the inactivity period (30–999 days)
  2. In the same section, check Require passwords for publicly shared boards

Source: Sharing policy on Enterprise Plan (content verified via the vendor’s help-center API, 2026-09).

Code Implementation

Code Pack: API Script
hth-miro-2.01-audit-sharing-policy.sh View source on GitHub ↗
# Per team: public-link sharing must be not_allowed, and sharing must be held to
# an allowlist of domains. A missing field is unreadable, never compliant.
audit_team() {
  local tid="$1" pol pub dom ndom org issues="" review=""
  api_get "/v2/orgs/{org_id}/teams/{team_id}/settings" "/v2/orgs/${ORG_ID}/teams/${tid}/settings"
  pol=$(printf '%s' "${BODY}" | jq -c '.teamSharingPolicySettings // empty')
  [ -n "${pol}" ] || precondition "team settings carry no teamSharingPolicySettings object"
  pub=$(printf '%s' "${pol}" | jq -r '.sharingViaPublicLink // "unknown"')
  dom=$(printf '%s' "${pol}" | jq -r '.restrictAllowedDomains // "unknown"')
  ndom=$(printf '%s' "${pol}" | jq -r '(.allowListedDomains // []) | length')
  org=$(printf '%s' "${pol}" | jq -r '.sharingOnOrganization // "unknown"')

  case "${pub}" in
    not_allowed) ;;
    allowed)              issues="${issues} public-link-sharing-allowed" ;;
    allowed_with_editing) issues="${issues} public-link-EDITING-allowed" ;;
    *)                    issues="${issues} sharingViaPublicLink-unreadable(${pub})" ;;
  esac
  case "${dom}" in
    enabled)                           [ "${ndom}" -gt 0 ] || issues="${issues} domain-restriction-with-empty-allowlist" ;;
    enabled_with_external_user_access) [ "${ndom}" -gt 0 ] || issues="${issues} domain-restriction-with-empty-allowlist"
                                       review="${review} external-users-bypass-allowlist" ;;
    disabled)                          issues="${issues} no-domain-restriction" ;;
    *)                                 issues="${issues} restrictAllowedDomains-unreadable(${dom})" ;;
  esac

  printf '  team …%s  sharingViaPublicLink=%s restrictAllowedDomains=%s allowListedDomains=%s sharingOnOrganization=%s  %s%s\n' \
    "${tid: -6}" "${pub}" "${dom}" "${ndom}" "${org}" \
    "$([ -z "${issues}" ] && echo OK || echo "FINDING:${issues}")" \
    "$([ -z "${review}" ] || echo "  REVIEW:${review}")"
  [ -z "${issues}" ] || FINDINGS=$((FINDINGS + 1))
}

The pack reads each team’s teamSharingPolicySettings. It cannot see the Company-level Boards can be shared publicly toggle, which has no endpoint, so a team whose own setting still allows public links is reported even when the Company toggle masks it. That team setting takes effect again the moment someone turns the Company toggle back on.


2.2 Board Copying and Export Controls

Profile Level: L2 (Walk) NIST 800-53: AC-21

Description

Restrict who can copy and export board content — in particular whether non-team members (visitors and guests) may copy at all — and set a restrictive default for newly created boards, so board content cannot be duplicated or extracted outside Miro’s access controls.

Rationale

Why This Matters:

  • Copies and exports create offline duplicates that escape Miro’s sharing permissions, audit logging, and revocation
  • Allowing non-team members to copy hands visitors and guests a one-click path to take internal design content with them
  • Setting the default copying permission for new boards closes the window where a freshly created board is permissive before anyone reviews it
  • Restricting copying reduces the value of a compromised or over-shared board, since access cannot be turned into portable files

Attack Prevented: Data exfiltration, bulk content extraction, insider data theft, loss of access control over copied content

Correction (2026-08): there is no “Export restrictions” or high-resolution export limit under Company Settings → Security. The real control is the per-team Copying Content setting in the Admin Console, which governs copying and exporting of boards and content. Earlier revisions of this guide described a setting that does not exist. Source: How to allow or restrict copying and exporting boards and content (content verified via the vendor’s help-center API, 2026-08).

ClickOps Implementation

Step 1: Set Team-Level Copying Permissions

  1. Navigate to: Admin Console → Teams
  2. Click the row for {Team name}, then open the Settings tab
  3. Scroll to Content Security
  4. For Copying Content, specify whether only team members — or anyone in the organization — can copy board content
  5. Set Default setting for copying content for newly created boards (settings save automatically)

Step 2: Understand the Downstream Effect

  1. When non-team members are not allowed to copy, the Anyone with board access option disappears from individual board sharing settings, so visitors and guests cannot copy
  2. Board owners and co-owners can still tighten (but not loosen beyond the admin setting) copying on a board via Share → Sharing settings → Who can copy board content
  3. On Free plans, copying is enabled by default and cannot be modified — this control requires Starter or above

Code Implementation

Code Pack: API Script
hth-miro-2.02-audit-copy-access.sh View source on GitHub ↗
# Per team: the default copy level for new boards must not be "anyone", and the
# ceiling must keep copy permission inside the team.
audit_team() {
  local tid="$1" lvl lim issues=""
  api_get "/v2/orgs/{org_id}/teams/{team_id}/settings" "/v2/orgs/${ORG_ID}/teams/${tid}/settings"
  lvl=$(printf '%s' "${BODY}" | jq -r '.teamCopyAccessLevelSettings.copyAccessLevel // "unknown"')
  lim=$(printf '%s' "${BODY}" | jq -r '.teamCopyAccessLevelSettings.copyAccessLevelLimitation // "unknown"')

  case "${lvl}" in
    team_members|team_editors|board_owner*) ;;
    anyone) issues="${issues} anyone-with-board-access-can-copy-new-boards" ;;
    *)      issues="${issues} copyAccessLevel-unreadable(${lvl})" ;;
  esac
  case "${lim}" in
    team_members) ;;
    anyone) issues="${issues} non-team-users-can-be-granted-copy" ;;
    *)      issues="${issues} copyAccessLevelLimitation-unreadable(${lim})" ;;
  esac

  printf '  team …%s  copyAccessLevel=%s copyAccessLevelLimitation=%s  %s\n' \
    "${tid: -6}" "${lvl}" "${lim}" \
    "$([ -z "${issues}" ] && echo OK || echo "FINDING:${issues}")"
  [ -z "${issues}" ] || FINDINGS=$((FINDINGS + 1))
}

The console setting exists from Starter, but the API that reads it (teamCopyAccessLevelSettings on the team-settings endpoint) is Enterprise-only. On Starter and Business, check the setting in the console.

Validation & Testing

As a guest on a board, open Share → Sharing settings and confirm the copy option is absent; then attempt a copy and confirm it is blocked.


2.3 Classify Boards and Enforce Guardrails

Profile Level: L3 (Run) NIST 800-53: RA-2, AC-16, AC-21

Description

Use Miro data classification to label boards by sensitivity, enable auto-classification and Microsoft Purview label import where applicable, and — with the Enterprise Guard add-on — attach Intelligent Guardrails so classified boards automatically lose the ability to be shared publicly, replicated, processed by Miro AI, or reached through the Miro MCP server.

Rationale

Why This Matters:

  • Classification labels alone are advisory: Miro documents that a label “has no impact on the board sharing settings,” so labels without guardrails do not stop anything
  • Intelligent Guardrails turn a classification into enforcement — automatically restricting sharing at public, team, and organization level and blocking content replication on sensitive boards
  • The Block Miro AI usage and Block Miro MCP Access guardrails close the newest exposure path, where AI agents and programmatic MCP clients read classified board content without the sharing controls applying
  • Auto-classification and Purview label import keep coverage current instead of relying on every board owner to label correctly

Attack Prevented: Over-sharing of sensitive boards, content replication out of controlled boards, AI/agent exfiltration of classified content via Miro AI or MCP, drift between Miro labels and enterprise data classification

Prerequisites

  • Enterprise plan for data classification (Company Admin role)
  • Enterprise Guard add-on for Intelligent Guardrails, auto-classification, and MCP/AI blocking

ClickOps Implementation

Step 1: Set Up Classification Labels

  1. Navigate to: Admin Console → Settings → Classification (with Enterprise Guard: Settings → Enterprise Guard → Classification)
  2. Select Set up classification to activate labels for the organization
  3. Select Edit classification levels to customize the four default labels or Add level (up to 30), setting Level, Name, Description, Badge color, and an optional Link to guidelines
  4. Set a default label for all new boards, then click Publish — changes stay in draft until published

Step 2: Define Guardrails

  1. In the Enterprise Guard settings, define guardrails per classification level: restrict sharing (public / team / organization), restrict content replication, Block Miro AI usage, Block Miro MCP Access
  2. Choose the rollout mode: default mode leaves active sharing on existing boards untouched; Apply guardrails in strict mode overrides all active sharing options and can immediately remove access for some users
  3. Configure auto-classification rules and, where you use Microsoft Purview, import Purview sensitivity labels so Miro classification follows the enterprise scheme

Code Implementation

Code Pack: API Script
hth-miro-2.03-audit-data-classification.sh View source on GitHub ↗
# Organization first (is classification on, is there a default label), then
# every team (is it on for the team, does the team assign a default label).
audit_org() {
  local enabled nlabels def issues=""
  api_get "/v2/orgs/{org_id}/data-classification-settings" "/v2/orgs/${ORG_ID}/data-classification-settings"
  enabled=$(printf '%s' "${BODY}" | jq -r 'if .enabled == true then "true" elif .enabled == false then "false" else "unknown" end')
  nlabels=$(printf '%s' "${BODY}" | jq -r '(.labels // []) | length')
  def=$(printf '%s' "${BODY}" | jq -r '[(.labels // [])[] | select(.default == true) | .name] | first // ""')
  [ "${enabled}" = "true" ] || issues="${issues} classification-not-enabled(${enabled})"
  [ -n "${def}" ] || issues="${issues} no-default-label"
  printf '  organization  enabled=%s labels=%s default=%s  %s\n' "${enabled}" "${nlabels}" "${def:-none}" \
    "$([ -z "${issues}" ] && echo OK || echo "FINDING:${issues}")"
  [ -z "${issues}" ] || FINDINGS=$((FINDINGS + 1))
}

audit_team() {
  local tid="$1" enabled deflabel issues=""
  api_get "/v2/orgs/{org_id}/teams/{team_id}/data-classification-settings" "/v2/orgs/${ORG_ID}/teams/${tid}/data-classification-settings"
  enabled=$(printf '%s' "${BODY}" | jq -r 'if .enabled == true then "true" elif .enabled == false then "false" else "unknown" end')
  deflabel=$(printf '%s' "${BODY}" | jq -r '.defaultLabelId // "" | tostring')
  [ "${enabled}" = "true" ] || issues="${issues} team-classification-not-enabled(${enabled})"
  [ -n "${deflabel}" ] || issues="${issues} no-default-label-for-new-boards"
  printf '  team …%s  enabled=%s defaultLabel=%s  %s\n' "${tid: -6}" "${enabled}" "$([ -n "${deflabel}" ] && echo set || echo none)" \
    "$([ -z "${issues}" ] && echo OK || echo "FINDING:${issues}")"
  [ -z "${issues}" ] || FINDINGS=$((FINDINGS + 1))
}

The pack proves the labelling half: classification is on for the organization and every team, and new boards get a default label. Intelligent Guardrails are ClickOps only. Miro exposes no write interface for them, and the REST API reference has no guardrail endpoint (Miro REST API reference, 2026-09-24).

Validation & Testing

Classify a test board at your most restrictive level, then attempt to share it publicly, duplicate it, and invoke Miro AI on it — each should be blocked. Boards created before the feature was enabled show as not classified, so treat pre-existing boards as an explicit remediation backlog. Sources: Data classification, Intelligent Guardrails overview, Import Microsoft Purview sensitivity labels (content verified via the vendor’s help-center API, 2026-08).


3. Integration Security

3.1 Manage Apps

Profile Level: L1 (Crawl) NIST 800-53: CM-7

Description

Audit installed Miro apps, remove unused integrations, and require admin approval before new apps can be installed so third-party access to boards is reviewed and minimized.

Rationale

Why This Matters:

  • Installed apps receive OAuth access to board content and can read or export data on a user’s behalf
  • Unvetted or abandoned apps expand the attack surface and may hold excessive permissions
  • Requiring admin approval prevents users from granting third parties access without review
  • Removing unused apps eliminates standing integration access that could be abused if the vendor is compromised

Attack Prevented: Malicious or over-permissioned OAuth apps, supply-chain compromise, unauthorized data access, shadow-IT integrations

Correction (2026-08): app governance is plan-split, and the admin-approval workflow is Enterprise-only. Below Enterprise, Team Admins get exactly one lever — whether non-admin team members may install apps at all. There is no approved-apps allowlist and no request/approval queue on Starter or Business, so “require admin approval” is not achievable there; the honest control on those plans is to disable non-admin installation and have an admin install a vetted set. Organization-level app management, the company-approved list, pre-adding/preauthorizing apps, and the App Request flow all require Enterprise. Sources: App management, How to install apps, App request flow (content verified via the vendor’s help-center API, 2026-08).

ClickOps Implementation

Step 1: Audit Installed Apps

  1. Navigate to: Team settings → Apps & Integrations for team-installed apps (Business and Enterprise)
  2. Navigate to: Company settings → Apps and integrations → Apps for the organization view (Enterprise)
  3. Review every installed app and its permissions; uninstall what is unused

Step 2: Restrict Who Can Install (Business and Enterprise)

  1. As a Team Admin, configure whether non-admin team members are allowed to install apps
  2. Note the coupled behavior: non-admin users cannot uninstall apps if they are not allowed to install them

Step 3: Build an Approved-Apps List (Enterprise only)

  1. Navigate to: Company settings → Apps and integrations → Apps → Manage apps
  2. Toggle on Restrict members to add apps so only company-approved apps can be added
  3. Pre-add and preauthorize the approved set for all teams or specific teams
  4. Review pending requests at Company settings → Apps and integrations → Apps → App Requests — all Company Admins are emailed on each request, and approving a request approves the app for other users too

Automation: ClickOps only — Miro exposes no write interface for this setting (Miro REST API reference, 2026-09-24). The reference has no app-management, allowlist or app-request endpoint.

Validation & Testing

As a non-admin test user, attempt to install an unapproved Marketplace app. On Enterprise with the restriction enabled, the install should convert into a request; on Business with non-admin installs disabled, it should be blocked outright.


3.2 API Token Security

Profile Level: L1 (Crawl) NIST 800-53: IA-5

Description

Audit the access tokens your Miro apps hold and revoke the ones you no longer need, review installed apps, limit token scopes, and rotate credentials periodically to control programmatic access to Miro.

Rationale

Why This Matters:

  • Access tokens are long-lived credentials that bypass interactive login and MFA if they leak
  • Over-scoped tokens grant far more API access than the integration needs, widening the impact of a leak
  • Unused or unrotated tokens accumulate as forgotten standing access that attackers can reuse
  • Periodic rotation and revocation limit how long a stolen token remains valid

Attack Prevented: Token theft and replay, over-privileged API access, credential leakage via code or logs, persistent unauthorized access

Prefer expiring access tokens. Miro’s OAuth documentation states that Miro recommends expiring access tokens: by default an access token is valid for 60 minutes and is issued with a refresh token valid for 60 days, and each refresh returns a new access token plus a new refresh token, resetting the 60-day window. The non-expiring token model does not age out on its own, so a leaked non-expiring token stays usable until someone notices and revokes it. Build integrations on the expiring model and treat any surviving non-expiring token as a rotation backlog item. Source: Getting started with OAuth 2.0.

ClickOps Implementation

Step 1: Review the Apps You Created

  1. Sign in to Miro and open Your apps, which lists the Miro apps you created and their credentials (Getting started with OAuth 2.0)
  2. For each app, confirm it is still in use and that it requests only the scopes it needs (see Step 4)
  3. End tokens you no longer need with Miro’s revoke endpoint (Revoke token (v2)). Revocation is a write, so the read-only pack below does not do it

Step 2: Review Installed Apps

  1. Navigate to: Team settings → Apps & Integrations, which lists every app installed for your team or by you personally
  2. Select an app you no longer use and click Uninstall for team (or Uninstall for me). Source: How to install apps

Step 3: OAuth App Security

  1. Grant the minimum scopes the integration actually needs
  2. Rotate tokens periodically, and migrate any non-expiring tokens to the expiring model

Step 4: Watch the High-Impact Scopes

  1. Several Miro scopes grant capabilities that no control in this guide otherwise constrains — treat a grant of any of them as a privileged-access decision:
Scope What it grants Availability
sessions:delete Ends all active Miro sessions across devices for a user Enterprise
boards:export Exports boards across the organization as PDF with comments and talktrack Enterprise
contentlogs:export Exports all activity on all boards in the organization Enterprise Guard
organizations:cases:management Accesses case and legal-hold information for eDiscovery Enterprise Guard
auditlogs:read Reads the organization’s audit events (see 4.1) Enterprise
  1. Review which registered apps hold these scopes and remove them where the integration’s function does not require them

Source: Permission scopes.

Code Implementation

Code Pack: API Script
hth-miro-3.02-audit-token-scopes.sh View source on GitHub ↗
# Read the token's own context, then compare its scopes against the high-impact
# list and, when one is declared, against the integration's allowlist.
api_get "/v1/oauth-token" "/v1/oauth-token"
printf '%s' "${BODY}" | jq -e '.scopes | type == "array" and length > 0' >/dev/null 2>&1 \
  || precondition "token context returned no scopes array — cannot judge the token"

SCOPES=$(printf '%s' "${BODY}" | jq -r '.scopes[] | tostring' | sort -u) \
  || precondition "could not read the token's scopes"
# The comparisons run in jq, which has no "no match" exit status: an empty
# result exits 0 and any non-zero exit is an error. (grep exits 1 for no match
# and 2 for an error, so "|| true" would read a failed comparison as no finding.)
HIGH=$(printf '%s' "${BODY}" | jq -r --arg list "${MIRO_HIGH_IMPACT_SCOPES}" \
  '($list | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $l
   | [.scopes[] | tostring | select(IN($l[]))] | unique | .[]') \
  || precondition "could not compare the token's scopes against the high-impact list"
EXTRA=""
if [ -n "${MIRO_ALLOWED_SCOPES}" ]; then
  EXTRA=$(printf '%s' "${BODY}" | jq -r --arg list "${MIRO_ALLOWED_SCOPES}" \
    '($list | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $l
     | [.scopes[] | tostring | select(IN($l[]) | not)] | unique | .[]') \
    || precondition "could not compare the token's scopes against MIRO_ALLOWED_SCOPES"
fi

echo "Miro 3.2 — access-token scope review"
echo "  token type: $(printf '%s' "${BODY}" | jq -r '.type // "unknown"')"
echo "  scopes ($(printf '%s\n' "${SCOPES}" | grep -c .)): $(printf '%s' "${SCOPES}" | tr '\n' ' ')"
FINDINGS=0
if [ -n "${HIGH}" ]; then
  echo "  FINDING: high-impact scopes granted: $(printf '%s' "${HIGH}" | tr '\n' ' ')"
  FINDINGS=$((FINDINGS + 1))
fi
if [ -n "${EXTRA}" ]; then
  echo "  FINDING: scopes outside MIRO_ALLOWED_SCOPES: $(printf '%s' "${EXTRA}" | tr '\n' ' ')"
  FINDINGS=$((FINDINGS + 1))
fi
[ -n "${MIRO_ALLOWED_SCOPES}" ] || echo "  NOTE: set MIRO_ALLOWED_SCOPES to the scopes this integration needs to check least privilege"

Run the pack once per integration token. It asks Miro what that token can do (GET /v1/oauth-token) and flags the high-impact scopes above, or any scope outside MIRO_ALLOWED_SCOPES when you declare the integration’s allowlist. It inspects only the token it is given.


3.3 Scope Miro AI Capabilities

Profile Level: L2 (Walk) NIST 800-53: CM-7, AC-3

Description

Use the Miro AI admin controls in the Admin Console to decide which AI capability categories exist in the organization and who may use them — Everyone, No one, or Specific teams — and, where the Enterprise Guard or AI Workflows add-on is present, to make that decision per individual feature rather than per category.

Rationale

Why This Matters:

  • AI capabilities read board content to produce their output, so enabling them broadly widens who and what can process sensitive diagrams and roadmaps
  • Category-level and feature-level scoping lets you enable low-risk capabilities (for example text editing) while removing higher-risk ones, instead of an all-or-nothing decision
  • Restricting to Specific teams confines AI processing to teams whose boards you have already reviewed, which is the practical way to pilot AI without exposing regulated content
  • Admins can view which models power each AI feature, which is the input a third-party-risk review actually needs

Attack Prevented: Uncontrolled AI processing of sensitive board content, shadow AI usage inside sanctioned tooling, over-broad feature rollout without review

Prerequisites

  • Enterprise plan, Company Admin role
  • Enterprise Guard or Miro AI Workflows add-on for per-feature (rather than per-category) control

ClickOps Implementation

Step 1: Set Capability Scope

  1. Navigate to: Admin Console → Miro AI → Capabilities
  2. For each capability, choose Everyone, No one (confirm with Remove access), or Specific teams (select teams, then Save)
  3. Note that Everyone overrides any team-level restriction, and deactivating all capabilities disables Create with AI on the board

Step 2: Apply Per-Feature Control (add-on)

  1. Turn on the Applied per feature option for a capability
  2. Enable or remove individual features inside the category — for example enable Create images with AI while disabling Remove background

Step 3: Pair with Classification

  1. Where boards are classified, use the Block Miro AI usage guardrail (see 2.3) so classified content is excluded from AI interaction regardless of the capability scope

Automation: ClickOps only — Miro exposes no write interface for this setting (Miro REST API reference, 2026-09-24). The only AI endpoint, GET /v2/orgs/{org_id}/ai-interaction-logs (Enterprise Guard, scope aiinteractionlogs:read), reads interaction logs. It configures nothing.

Validation & Testing

As a member of a team excluded from a capability, confirm the corresponding AI entry point is unavailable on a board. Sources: Miro AI admin controls, Miro AI granular admin controls (content verified via the vendor’s help-center API, 2026-08).


4. Monitoring & Detection

4.1 Audit Logs (Enterprise)

Profile Level: L1 (Crawl) NIST 800-53: AU-2, AU-3

Description

Enable Miro Enterprise audit logs, review activity events, and forward them to a SIEM so security-relevant actions across boards and the team space are recorded and monitored.

Rationale

Why This Matters:

  • Audit logs provide the evidence trail needed to detect misuse, investigate incidents, and meet compliance requirements
  • Without centralized logging, account compromise, mass sharing, or bulk exports go unnoticed
  • SIEM forwarding enables alerting on anomalous activity in near real time instead of after the fact
  • Retained logs support forensic reconstruction of what an attacker accessed or changed

Attack Prevented: Undetected account compromise, insider misuse, delayed breach detection, gaps in forensic evidence

Audit events are deleted on a schedule, so scheduled export is mandatory, not optional. Miro stores audit events for 180 days by default. Admins can set retention to 30, 90, 180 or 365 days, indefinite retention has been deprecated, and “Once audit logs are deleted, they can’t be recovered.” The API returns only what is still inside that window: it “retrieves a page of audit events within your organization’s configured audit log retention period.” The console preview is shorter still, covering only the last 90 days. A retention requirement longer than the configured period therefore needs a scheduled pull into the SIEM or archive. Sources: Audit logs (content verified via the vendor’s help-center API, 2026-09), Get audit logs (Enterprise).

ClickOps Implementation

Step 1: Access Audit Logs

  1. Navigate to: Company Settings → Security → Audit logs (Audit logs)
  2. Filter by Date range, Actor, Event category and Event, then click View events. The preview covers the last 90 days only
  3. Click Export to CSV for any period you must keep outside Miro. The file stays downloadable for 24 hours, and only one export file per organization exists at a time

Step 2: Set the Retention Period

  1. On the same page, open the Settings tab
  2. Choose a retention period (30, 90, 180 or 365 days) at least as long as your export cadence needs, and confirm. Events past the period are deleted and cannot be recovered

Step 3: Automate Export to the SIEM

  1. Register an app and grant the auditlogs:read scope (Enterprise)
  2. Schedule a pull against the Enterprise audit logs endpoint on a cadence well inside the retention period: a daily or weekly job, not an ad-hoc query
  3. Alert on job failure: a silently broken export means events age out and are deleted

Code Implementation

Code Pack: API Script
hth-miro-4.01-pull-audit-logs.sh View source on GitHub ↗
# Walk the window page by page, emit each event as one JSON line, and refuse to
# report success on a window that returned nothing or was cut short.
EVENTS=0; PAGES=0; CURSOR=""; PREV=""
BASE_Q="createdAfter=$(jq -rn --arg v "${SINCE}" '$v|@uri')&createdBefore=$(jq -rn --arg v "${UNTIL}" '$v|@uri')&limit=100&sorting=ASC"
while :; do
  Q="${BASE_Q}"
  [ -z "${CURSOR}" ] || Q="${Q}&cursor=$(jq -rn --arg c "${CURSOR}" '$c|@uri')"
  api_get "/v2/audit/logs" "/v2/audit/logs?${Q}"
  printf '%s' "${BODY}" | jq -e '.data | type == "array"' >/dev/null 2>&1 || precondition "audit page has no data array"
  printf '%s' "${BODY}" | jq -c '.data[]'
  EVENTS=$((EVENTS + $(printf '%s' "${BODY}" | jq '.data | length')))
  PAGES=$((PAGES + 1))
  PREV="${CURSOR}"
  CURSOR=$(printf '%s' "${BODY}" | jq -r '.cursor // ""')
  [ -n "${CURSOR}" ] || break
  [ "${CURSOR}" != "${PREV}" ] || precondition "audit pagination repeated its cursor after ${EVENTS} events — window NOT complete"
  [ "${PAGES}" -lt "${MIRO_AUDIT_MAX_PAGES}" ] || precondition "audit pull hit ${MIRO_AUDIT_MAX_PAGES} pages after ${EVENTS} events — window NOT complete; narrow it"
done

echo "Miro 4.1 — audit events ${SINCE} .. ${UNTIL}: ${EVENTS} events in ${PAGES} page(s)" >&2
if [ "${EVENTS}" -eq 0 ] && [ "${MIRO_AUDIT_ALLOW_EMPTY:-0}" != "1" ]; then
  echo "  FINDING: zero audit events in the window — treat as a broken export until proven idle" >&2
  exit 1
fi

The pack walks the documented cursor through a time window (default: the last 24 hours) and writes each event as one JSON line for the SIEM. It exits non-zero on an empty window or a truncated pull, so a broken schedule cannot pass as a quiet day.

Detection Focus

  • Export-pipeline gaps: alert when the scheduled audit pull returns zero events or fails, since events past the retention period are deleted and cannot be recovered
  • Sharing and copying spikes: bursts of board-share or content-copy events, especially involving guests or non-team members (see 2.2)
  • App and token activity: new app installs, app approvals, and SCIM token generation — correlate against the approved-apps list from 3.1
  • Authentication anomalies: SSO configuration changes, 2FA disablement, and logins from users on unverified domains who bypass SSO enforcement

Appendix A: Edition Compatibility

Miro’s current plan tiers are Free, Starter, Business, and Enterprise — there is no “Team” plan. Verified against Miro pricing, 2026-08. The 2FA, multi-IdP, sharing-policy and REST API rows were verified against the help-center and API reference pages cited in 1.1, 2.1 and the Scope note, 2026-09.

Control Free Starter Business Enterprise
SAML SSO (1.1) ❌ ❌ ✅ ✅
Multiple identity providers (1.1) ❌ ❌ ❌ Private beta
2FA enforcement for magic-link sign-ins (1.1) ❌ ✅ ✅ ✅
Idle Session Timeout (1.3) ❌ ❌ ❌ ✅
SCIM provisioning (1.4) ❌ ❌ ❌ ✅
Domain control ❌ ❌ ❌ ✅
Org-level sharing policy: public links, allowed domains (2.1) ❌ ❌ ❌ ✅
Copying/export permissions (2.2) ❌ ✅ ✅ ✅
Data classification (2.3) ❌ ❌ ❌ ✅
Intelligent Guardrails / auto-classification (2.3) ❌ ❌ ❌ Enterprise Guard add-on
Team-level app management (3.1) ❌ ❌ ✅ ✅
Approved-apps list + App Request flow (3.1) ❌ ❌ ❌ ✅
Miro AI admin controls (3.3) ❌ ❌ ❌ ✅
Per-feature Miro AI control (3.3) ❌ ❌ ❌ Enterprise Guard / AI Workflows add-on
Audit logs + audit API (4.1) ❌ ❌ ❌ ✅
REST API reads used by the Code Packs (1.2, 2.1–2.3, 4.1) ❌ ❌ ❌ ✅

Appendix B: References

Official Miro Documentation:

Miro’s help center returns HTTP 403 to non-browser fetchers. Every help.miro.com article above was content-verified through Miro’s own first-party help-center API (help.miro.com/api/v2/help_center/..., published non-draft article bodies) in 2026-08, and the articles added in 0.3.0 on 2026-09-24; the human-readable article URLs are cited here.

API Documentation:

Compliance Frameworks:

Security Incidents:

  • No major public security incidents identified for Miro as of this revision.

Changelog

Date Version Maturity Changes Author
2026-09-25 0.3.0 ai-drafted validate-hth-guide run, fix phase. No live surface was exercised (0 VERIFIED-LIVE): the Miro console was behind a sign-in wall, and no credential or tenant was available. Maturity is unchanged. New Code Packs: six read-only api/ audit packs, for 1.2 (team roles and invitation policy), 2.1 (team sharing policy), 2.2 (copy access), 2.3 (data classification), 3.2 (token scope review) and 4.1 (audit-log pull to NDJSON). Each follows the documented Enterprise REST endpoints and fails closed on any non-200, an empty listing or a truncated pull. They were tested offline against schema-shaped fixtures, not against a tenant. Automation verdicts: 1.1, 1.3, 1.4, 3.1 and 3.3 now carry an evidenced ClickOps-only line, and the Scope section records the automation surface. Corrections: 1.1 SSO path (Single sign-on, toggle SSO/SAML, per-plan breadcrumbs), multi-IdP is a private beta, 2FA enforcement paths per plan (Starter, Business, Enterprise) and its magic-link-only coverage, the SSO quote now lists magic link where it said password, and unverified-domain users sign in by magic link or social account (Miro removed password sign-in). 2.1 real path Company settings → Security → Sharing, toggle Boards can be shared publicly (Enterprise), plus allowed domains, the team-level override and link expiry/passwords. 3.2 heading renamed to ClickOps Implementation, Your apps and Team settings → Apps & Integrations replace the undocumented “Profile → Apps & integrations”, and “personal access tokens”, a term Miro does not use, is gone. 4.1 console path cited, Export to CSV, and the retention Settings tab, and Detection Focus names SCIM token generation (a logged event) in place of personal-token creation. The stale “last 90 days” API quote is replaced: the endpoint now covers the configured retention period, 180 days by default. Appendix A adds 2FA, sharing-policy and REST API rows. After an independent audit of the run: 1.2 Step 2 cites Miro’s per-plan invitation paths and covers who can invite, guests and the team invite link. 1.4 names Company settings → Enterprise integrations → SCIM Provisioning and token rotation. The Scope section records the SIEM surface and why no detection-rule pack ships. The 3.2 pack now fails closed when a scope comparison errors (it had reported OK) and accepts comma-space scope lists, and 1.2’s role counts fail closed the same way. Claude Code (Opus 5.5)
2026-08-08 0.2.0 ai-drafted Currency pass: corrected SAML SSO to Business+Enterprise (multi-IdP Enterprise-only), corrected 2.2 to the real Copying Content control (no “Export restrictions” setting exists), corrected 3.1 app governance plan split (no allowlist below Enterprise), corrected plan tiers (no “Team” plan; domain control Enterprise-only); added 1.3 Idle Session Timeout, 1.4 SCIM, 2.3 classification + Intelligent Guardrails, 3.3 Miro AI admin controls; added 90-day audit-API window and Detection Focus content; added expiring-OAuth-token guidance and high-impact scope table; rebuilt Appendix A and removed Trust Center / marketing sources from Appendix B. help.miro.com articles 403 non-browser fetchers and were content-verified via Miro’s first-party help-center API. Tier 2: no CIS Benchmark, DISA STIG, or CISA SCuBA baseline exists for Miro (confirmed zero). Tier 3/4: not surveyed in this pass. Claude Code (Opus 5)
2026-06-29 0.1.1 ai-drafted Add cheat-sheet Description and Rationale for all controls Claude Code (Opus 4.8)
2025-12-14 0.1.0 ai-drafted Initial Miro hardening guide Claude Code (Opus 4.5)

Contributing

Found an issue or want to improve this guide?