SharePoint Sharing/Hero Link inventory
This post follows on from SharePoint Hero Links: What admins/adoption teams need to know? and focuses on the technical difference between legacy sharing links and hero links.
To validate what SharePoint is now generating behind the scenes, I adapted my earlier script from Converting SharePoint Sharing Links to Canonical URLs to validate whether Hero link exists or not even there are no unique permissions on the file/folder.
The key behaviour change is this: legacy sharing links were typically created only when a user accidentally shared an item (e.g. ‘people in your org’ link), or deliberately as with ‘specific people’ links. Hero links behave differently. SharePoint provisions them much more broadly, including for files and folders that do not have unique permissions and the single hero link would be amended for broader or restricted access.
That becomes visible when users select Copy link. Instead of getting a file or folder path if though the default is set to ‘OnlyPeopleAdded’, the link is unique link with a randomly generated ID which makes you hard to decipher which file the link refers to when shared /embedded in other locations which may make bulk updating those links difficult in case of migration [Converting SharePoint Sharing Links to Canonical URLs]https://reshmeeauckloo.com/posts/powershell-sharepoint-getfileurl-basedon-sharingurl/. For admins, content owners, and adoption teams, that makes management complex.
Hero link behavior with Copy link
With hero links enabled, Copy link returns the hero link rather than the underlying path even the default is set to ‘OnlyPeopleAdded’. In practice, that means a user often cannot tell what the link points to until they open it in a browser.

Folder hero link format:
https://
e.g
https://reshmeeauckloo.sharepoint.com/:f:/r/sites/Company311/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FCompany311%2FShared%20Documents%2FTest1%2FFolder%5F1&p=true&share=cgpOP4IqX7qjQJShmxNPxpbXEgUC4m6eDZEe5kQvKzmXP2djbA
File hero link example:
Format
https://
e.g.
https://reshmeeauckloo.sharepoint.com/:w:/s/Company311/cQoQe1VOYjDkTa-popFrkTxHEgUCS-JsIr5gEyqmZTI0xvg3SA
Legacy Copy link
If your sharing controls are configured well, the default sharing link is often set to People with existing access. In that case, Copy link is closer to the older behaviour and returns a path-based link instead of creating a new broad sharing link.

Example of a legacy copy link using People with existing access:
If the default sharing link is set to People in your organization or Anyone, then clicking Copy link creates a new sharing link instead:

What the inventory script shows
When I ran the inventory, the result was clear: a sharing link exists even when the item does not have unique permissions. That is the important distinction. A hero link is not a reliable indicator that someone intentionally shared the item or broke inheritance.

The script below enumerates file and folder sharing links and then checks whether the target item has unique permissions. That makes it easier to separate genuine permission exceptions from hero-link noise in your reporting. Alas it will take longer to report on sharing links only, it’s still good to validate whether there is actually unique permissions at the file/folder level to identify broken permission inheritance.
param(
[Parameter(Mandatory=$true)]
[string]$SiteUrl
)
function Get-ListItems_WithUniquePermissions{
param(
[Parameter(Mandatory)]
[Microsoft.SharePoint.Client.List]$List
)
$selectFields = "ID,HasUniqueRoleAssignments,FileRef,FileLeafRef,FileSystemObjectType"
$Url = $siteUrl + '/_api/web/lists/getbytitle(''' + $($list.Title) + ''')/items?$select=' + $($selectFields)
$nextLink = $Url
$listItems = @()
$Stoploop =$true
while($nextLink){
do{
try {
$response = invoke-pnpsprestmethod -Url $nextLink -Method Get
$Stoploop =$true
}
catch {
write-host "An error occured: $_ : Retrying" -ForegroundColor Red
$Stoploop =$true
Start-Sleep -Seconds 30
}
}
While ($Stoploop -eq $false)
$listItems += $response.value | where-object{$_.HasUniqueRoleAssignments -eq $true}
if($response.'odata.nextlink'){
$nextLink = $response.'odata.nextlink'
} else{
$nextLink = $null
}
}
return $listItems
}
connect-PnPOnline -Url $siteUrl
$dateTime = (Get-Date).toString("dd-MM-yyyy-hh-ss")
$invocation = (Get-Variable MyInvocation).Value
$directorypath = Split-Path $invocation.MyCommand.Path
$fileName = "SharedLinks-PnP" + $dateTime + ".csv"
$ReportOutput = $directorypath + "\Logs\"+ $fileName
$global:Results = @();
$ll = Get-PnPList -Includes BaseType, Hidden, Title,HasUniqueRoleAssignments,RootFolder | Where-Object {$_.Hidden -eq $False -and $_.Title -notin $ExcludedLists } #$_.BaseType -eq "DocumentLibrary"
Write-Host "Number of lists $($ll.Count)";
foreach($list in $ll)
{
$listUrl = $list.RootFolder.ServerRelativeUrl;
$ListItems_withUniquePermissions = Get-ListItems_WithUniquePermissions -List $list
$links = Get-PnPListItem -List $list.Title | Get-PnPFileSharingLink
$Folders= Get-PnPListItem -List $list.Title -Fields "FileRef", "FileLeafRef", "FileSystemObjectType" | Where-Object { $_.FileSystemObjectType -eq "Folder" }
$folderLinks = $folders | ForEach-Object {
Get-PnPFolderSharingLink -Folder $_['FileRef'] -ErrorAction SilentlyContinue
}
foreach($folderLink in $folderLinks)
{
if ($folderLink -eq $null) {
continue
}
# Check if ShareId already exists in global results
if ($global:Results | Where-Object { $_.ShareId -eq $folderLink.Id }) {
Write-Host "Skipping duplicate ShareId: $($folderLink.Id)" -ForegroundColor Yellow
continue
}
$isUnique = $false
$uniqueItem = $listItems_withUniquePermissions | Where-Object { $_.FileRef -eq $folderLink.Parent.ServerRelativeUrl }
if ($uniqueItem) {
$isUnique = $true
}
else {
$isUnique = $false
}
$result = New-Object PSObject -property $([ordered]@{
Name = $folderLink.Parent.Name
RelativeURL = $folderLink.Parent.ServerRelativeUrl
ShareId = $folderLink.Id
RoleList = $folderLink.Roles -join "|"
Users = $folderLink.GrantedToIdentitiesV2.User.Email -join "|"
ShareLinkUrl = $folderLink.Link.WebUrl
ShareLinkType = $folderLink.Link.Type
ShareLinkScope = $folderLink.Link.Scope
Expiration = $folderLink.ExpirationDateTime
BlocksDownload = $folderLink.Link.PreventsDowload
RequiresPassword = $folderLink.HasPassword
ContainsUniquePermissions = $isUnique
})
$global:Results +=$result;
}
foreach($ShareLink in $links)
{
# Skip if ShareLink is null
if ($ShareLink -eq $null) {
continue
}
# Check if ShareId already exists in global results
if ($global:Results | Where-Object { $_.ShareId -eq $ShareLink.Id }) {
Write-Host "Skipping duplicate ShareId: $($ShareLink.Id)" -ForegroundColor Yellow
continue
}
$uniqueItem = $listItems_withUniquePermissions | Where-Object { $_.FileRef -eq $ShareLink.Parent.ServerRelativeUrl }
$isUnique = $false
if ($uniqueItem) {
$isUnique = $true
}
else {
$isUnique = $false
}
$result = New-Object PSObject -property $([ordered]@{
Name = $ShareLink.Parent.Name
RelativeURL = $ShareLink.Parent.ServerRelativeUrl
ShareId = $ShareLink.Id
RoleList = $ShareLink.Roles -join "|"
Users = $ShareLink.GrantedToIdentitiesV2.User.Email -join "|"
ShareLinkUrl = $ShareLink.Link.WebUrl
ShareLinkType = $ShareLink.Link.Type
ShareLinkScope = $ShareLink.Link.Scope
Expiration = $ShareLink.ExpirationDateTime
BlocksDownload = $ShareLink.Link.PreventsDowload
RequiresPassword = $ShareLink.HasPassword
ContainsUniquePermissions = $isUnique
})
$global:Results +=$result;
}
}
$global:Results | Export-CSV $ReportOutput -NoTypeInformation
#Export-CSV $ReportOutput -NoTypeInformation
Write-host -f Green "Sharing Links Report Generated Successfully!"
Sample hero link output
Here is an example of a hero link returned by the script. One confusing detail is that the scope is shown as Anonymous, which can be misleading when you are trying to separate hero links from legacy sharing links in a report.
From my testing, the most reliable indicators of a hero link were:
- the scope is reported as anonymous
- the hero link does not have an expiry date
- the report shows the users field as blank even when additional users have been added

- ShareLinkType always set to View
At the moment, I have not found a better programmatic way to distinguish hero links from legacy links. This is an area where Microsoft could improve the experience by making hero links easier to identify and by handling expired links more clearly in the background. Also a way to easily get the file/folder path from sharing link id/
Name RelativeURL ShareId RoleList Users ShareLinkUrl ShareLinkType ShareLinkScope Test1 /sites/Company311/Shared Documents/Test1 2c9337df-c137-42e0-a9f8-2df51ef76558 Read https://reshmeeauckloo.sharepoint.com/:f:/r/sites/Company311/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FCompany311%2FShared%20Documents%2FTest1&p=true&share=cgrfN5MsN8HgQqn4LfUe92VYEgUC%2D5OkDIj%5FE8EVJvXIR%5F4eLA View Anonymous
Final takeaway
If you are reviewing sharing posture in SharePoint Online, do not assume that every sharing link in your export represents a deliberate sharing action or a permission break. Hero links change that assumption. You now need to distinguish between:
- links created as part of actual sharing activity
- links automatically provisioned by SharePoint as hero links
That distinction matters for governance reporting, user support, and any clean-up exercise based on exported sharing-link inventories.