Skip to content

ts storage-scope

Show and manage a storage scope — the named mount point that makes a cloud storage connection visible inside one workspace.

Usage

text
ts storage-scopes
ts storage-scopes <workspace>
ts storage-scope <id>
ts storage-scope <id> <property>
ts storage-scope new --file=<path>
ts storage-scope set <id> <property> <value>
ts storage-scope set <id> --file=<path>
ts storage-scope delete <id> [--force]

Description

A storage scope binds three things together: a workspace, a global storage connection made against a storage provider, and optionally a sub-folder of that connection. Members of the workspace then see it as a named folder. Its kind says what the scope is for — 0 for user files, the locations people keep documents in, and 1 for working storage, where software puts chat uploads and tool output. Kind is a label on the scope, not an access rule; surfaces decide what to do with it.

ts storage-scopes with no argument lists every scope you are allowed to see, across all workspaces. Give it a workspace — GUID or friendly id — to list just that one. The ID column holds the scope's friendly id, and both that and the scope's GUID work wherever <id> appears. In the list-all form the WORKSPACE column names the owning workspace; in the per-workspace form it simply echoes the workspace you asked for.

A scope is administered as a JSON document. ts storage-scope <id> prints it, new and set read it from --file, and - reads standard input.

For a single field there is a shorter path. ts storage-scope <id> <property> reads one field, and ts storage-scope set <id> <property> <value> writes one. The two write forms are complementary rather than alternatives: the accessor changes one field and leaves the rest of the document as it was, while --file replaces the whole document. All three of the fields a set applies — name, rootPath, and kind — are scalars, so the accessor covers most day-to-day changes; reach for --file when you are changing more than one of them at a time or working from a file you already have.

Property names are kebab-case and map to the document's camelCase key, so root-path addresses rootPath. Values are typed from the field being replaced — kind stays a number, so kind 1 writes 1 and not "1" — and you never have to know a field's wire type. A field that is currently null, such as url on a scope that is not on a file system provider, is the exception: there is nothing to infer a type from, so the value goes in as a string.

A structured field has no accessor form. connection is an object, so it is refused with a pointer at --file — which will not help either, since a scope cannot be re-pointed at another connection at all. A property the document does not carry is refused too. Both refusals happen before anything is written, so a typo cannot half-apply.

The accessor is a read-modify-write: it fetches the document, changes the one field, and sends the whole document back. That is safe here because the document round-trips untyped, so a field this version of ts does not know about is preserved rather than dropped. It does not widen what the server accepts, though: workspaceId, cloudStorageAccessTokenId, and friendlyId are discarded on a write, so naming one of those gets past the CLI, reports success, and changes nothing.

set applies three fields and ignores the rest. Only name, rootPath, and kind are read from the document; workspaceId, cloudStorageAccessTokenId, and friendlyId are accepted and discarded. A scope cannot be re-pointed at a different workspace or a different connection — delete it and create a replacement.

A printed document is not a create template. print emits workspaceId as the server's internal numeric id, while new expects workspaceId to be a workspace GUID or friendly id, so feeding a printed document straight into new is rejected as a malformed request. Write create documents by hand: workspaceId, cloudStorageAccessTokenId (the connection's id), name, and optionally friendlyId, rootPath, and kind.

A non-admin read hides the root path. Sanitizing is keyed on whether you are an administrator, not on how you signed in, so a Server Administrator's own ticket reads the real rootPath--local is not required for a round trip. A member with workspace access gets the document with rootPath blanked; the workspace id stays, because it is the workspace's public id rather than an internal one. Writes require an administrator either way, so the case that used to bite — reading a blanked path and writing it back, silently re-mounting the scope at the connection root — is no longer reachable by anyone who can write.

Creating a scope has more preconditions than the other document commands. The name must be unique within the workspace, and the friendly id — generated by slugifying the name when you omit it — must be unique across every scope on the server. The connection must be a global one; a per-user connection carries a single user's grant and would resolve for nobody else, so it is refused. Its provider must not have been deleted. And a rootPath that points at a sub-folder is refused for providers that can only be mounted at their root, because the boundary could not be enforced later.

ts storage-scope delete prompts unless you pass --force, and it is a soft delete. It is refused outright while a workspace has the scope set as its chat attachment storage; change that setting first. Everything else that reads a scope filters deleted rows out, so the mount disappears for workspace members as soon as the command returns.

Options

FlagDescriptionDefault/ValuesNotes
--fileJSON document to send.PATH, or - for standard inputRequired by new. set needs it only for the whole-document form.
--force, -fDelete without confirming.delete only. Also required when running non-interactively.
--localTarget the install on this machine with a service token.For administering the box you are on; not needed to read rootPath.

Output

print writes the document and nothing else, so it pipes cleanly:

json
{
  "id": "6b0d5a41-2c8e-4f13-9a77-0e5b3d1c8a92",
  "workspaceId": 12,
  "workspaceFriendlyId": null,
  "workspaceName": null,
  "cloudStorageAccessTokenId": "a41e7d90-55b2-4c08-8f3e-6d2a9b40c157",
  "name": "Team Files",
  "friendlyId": "team-files",
  "rootPath": "Projects/2026",
  "kind": 0,
  "url": null,
  "created": "2026-05-04T18:22:31.44Z",
  "deleted": null,
  "connection": {
    "rowGuid": "a41e7d90-55b2-4c08-8f3e-6d2a9b40c157",
    "name": "HQ OneDrive",
    "storageProvider": {
      "slug": "onedrive",
      "name": "OneDrive",
      "storageType": 0,
      "connectionMode": 1
    }
  }
}

workspaceFriendlyId and workspaceName are filled in only by the list-all form of ts storage-scopes; a single-scope read leaves them null. url is populated only for scopes on a file system provider, where it is the served path the scope is reachable at. Under --format=json the document is wrapped in the standard result envelope as result.entity, which is not what set consumes. Use the default text output for round trips.

A single-field read prints the bare value — Projects/2026, not "Projects/2026" — so it drops straight into a shell variable, which makes it the quickest way to confirm where a scope is mounted. Under --format=json that value comes back in the envelope under the document's own key, result.rootPath.

Examples

bash
# Every scope you can see, and which workspace each belongs to
ts storage-scopes

# Just one workspace's scopes
ts storage-scopes engineering

# Full names and root paths, untruncated
ts storage-scopes --no-trunc

# Where is one scope mounted?
ts storage-scope team-files root-path

# Rename it, or move it to another sub-folder — one field, no file
ts storage-scope set team-files name "Team Files (2026)"
ts storage-scope set team-files root-path Projects/2027

# Several fields at once: the document round trip
ts storage-scope team-files > scope.json
$EDITOR scope.json
ts storage-scope set team-files --file=scope.json

# Mount a global connection into a workspace
cat <<'JSON' | ts storage-scope new --file=-
{ "workspaceId": "engineering", "cloudStorageAccessTokenId": "a41e7d90-55b2-4c08-8f3e-6d2a9b40c157",
  "name": "Team Files", "rootPath": "Projects/2026", "kind": 0 }
JSON

# Unmount a scope
ts storage-scope delete team-files --force

# A member's read blanks rootPath — but a member cannot write a scope either,
# so the old re-mount-at-root trap is no longer reachable.

Exit Codes

0 on success. -1 when the server rejects the document — a printed document sent to new, a duplicate name or friendly id, an unrecognized kind, a per-user or provider-less connection, a sub-folder path on a provider that must be mounted at its root — and when a delete is blocked because a workspace uses the scope for chat attachments, when new is run without --file, when set is run with neither --file nor a property and value, or when a delete is declined or run non-interactively without --force. -1 also covers the accessor's own refusals: a property that is not a field of the scope, and a property holding an object or array, which is how connection comes back. -4 when no account is signed in. -5 when the account is not a Server Administrator, which affects new, set, and delete but not reads. -6 when no scope has that id, when a single-field read names a property the document does not carry, when the workspace named in a create does not exist, and when the scope exists but is in a workspace you cannot see — visibility failures are reported as absence on purpose.

  • ts storage-provider — The backend a scope's connection is made against; register it first.
  • ts workspace — The workspace a scope belongs to, and the source of the workspace argument.
  • ts share — The other way content leaves a workspace.