Docs/Lanes Link/Reference/Commands

Commands

Every command, its arguments, and its flags

Every command, its arguments, and its flags. lanes link help is the same list without the detail; Workflow walks the lifecycle in order.

Every command names what it acts on. --profile <name> and --workspace <name>, with no default and no environment variable (ADR-037). They are omitted from the tables below because they apply to almost everything; each entry says when a command wants only --profile, only --workspace, or neither.

An account and an endpoint belong to a workspace, not to a profile. connect, disconnect, start, outputs, deploy, mcp add, mcp stdio, setup plan, status, tools, audit, secrets, workspace and token name a --workspace alone. You authorise an account once per workspace rather than once per profile, and one endpoint serves every profile the workspace holds, so a --profile beside these reads as a narrowing that does not happen. deploy is the exception that proves it: --profile there is repeatable and selects which profiles to send.

What a profile holds still names it. grant and policy decide which of the workspace's accounts a profile reaches and what it may do with them, profile members decides who may consume it, and memory, tasks, assets, skills, vault, entities and identity are its own. All of them take --profile.

--json, --quiet and --help are accepted everywhere. Any other flag a command does not read is refused, with a guess when the spelling is close.

Values arrive on stdin. memory write, skills add, vault set and secrets set read from a pipe and refuse rather than hang when stdin is a terminal. tasks add is the exception: its notes are optional, so an empty pipe or a terminal simply means a task with no notes.

Three commands cannot be run at all, several documented flags are refused when you type them, and a few more are accepted and ignored. Each is marked broken in its own entry, and the tables carry only what works. #44 has the detail.


Everyday

What connecting something involves: with no argument, every provider and whether it is connected; with one, the console steps, the values, and the command to run.

--id <name>plan for a specific connection id
console
$ lanes link setup plan
$ lanes link setup plan gmail

Add an account. Run once per account; a second run adds a second account. <provider>.<id> re-authorises an existing one.

It ends by asking what to call the connection, offering the account it just resolved:

console
What should this be called? [[email protected]]:

Pressing Enter takes the account, which is the usual answer. Anything else is written to label and changes nothing else. See relabel below for why that is a separate field from account. The question is skipped when --label says it, when there is no terminal to ask, and for the second and third service of a family: connect icloud is one account, so it is named once rather than three times.

--id <name>the connection id, instead of one derived from the account
--display-name <text>whose account this is, for a provider that cannot report it
--label <text>what to call the connection, instead of being asked
--replaceask for the stored password or key again
--auth <method>which way in, where a provider offers two; oauth is the browser
--own-clientyour own OAuth client rather than this project's (older spelling of an --auth route)
--accept-broad-scopesagree in advance to scopes broader than the provider needs
--non-interactivetake every value from the credential store, or say what is missing
console
$ lanes link connect gmail
$ lanes link connect gmail --id work

--workspace never points at a running endpoint: connect --workspace cloud runs consent locally and writes the token into the deployment's store.

Remove one account, and delete the credential it authorised. The counterpart to connect, and the two edits it takes: the entry leaves connections: in the profile, and the credential leaves the workspace's store.

The key must be exact. connect gmail can create an account and choose the id; there is nothing to choose here, and a bare gmail with two accounts declared would be a command guessing which one to throw away, so it refuses and names them.

console
$ lanes link disconnect gmail.side --workspace local
$ lanes link disconnect gmail.side --workspace local --yes --json
$ lanes link disconnect gmail.side --workspace local --keep-credential

Three things it deliberately does not do.

It does not delete the state record. Reconcile marks an undeclared connection disabled rather than deleting it so the audit log keeps meaning something, and reaching past that to erase the record would undo the one guarantee the log offers. The next reconcile marks it, and the command says so.

It does not delete a credential something else still resolves to. A reference is per connection for an OAuth provider (<provider>/<id>), and shared for a manifest declaring credential_ref, deleting that one while a sibling resolves to it would take the sibling's credential with it, and the sibling would then report unauthorized for a connect nobody ran. When that happens the credential is kept and the output names what still needs it.

It does not touch the owner layer. memory, skills, vault, setup and identity hold no credential and are granted by a policy line this command does not touch, so removing the connection alone would leave the policy granting against nothing, which is wrong rather than merely untidy. It refuses and points at the file, where both lines are next to each other.

Rename what an account is called. The label is what status and any surface built on it shows; it is not an identifier, so nothing addresses the connection by it and changing one breaks nothing.

It writes label, never account, and the difference is not cosmetic. account is the identity the provider reported, and three things read it as one: settleIdentity matches on it to decide a second connect is a repair rather than a new account, the connection id is derived from it, and gmail.send_message writes it into a From header. Renaming through account, which this command did until the field existed, left a row the next connect gmail no longer recognised, and added a second one beside it.

console
$ lanes link relabel gmail.con1 "Work mail" --profile personal
$ lanes link relabel gmail.con1 Work mail --profile personal

Both forms work: an unquoted multi-word name is joined, because quoting is what someone remembers second and refusing it teaches nothing.

connect already asks for one and offers the account as the answer, so this is for changing your mind later, and for the surfaces that drive the CLI rather than type it. Allowed for the owner layer, unlike disconnect: a display name is harmless, and "Memory" is the operator's word for their own store.

The state store keeps the old name until the next reconcile, which updates it.

Declare a service that is not built in, and connect it. The provider is composed from two fixed lists, one connectivity type and one credential type, written to providers.d/<id>.yaml, and then connected exactly as a built-in is. The manifest is yours to edit afterwards; lanes link connect <id> re-reads it every time.

Omit a required value and it is asked for. --non-interactive names every missing one at once instead, with the command to re-run.

--connector <kind>mcp, http, imap, dav, fs
--auth <method>none, bearer, api-key, header, basic, oauth, strategy
--name <text>, --description <text>how the provider is labelled; the name defaults to the id read as words
--endpoint <url>mcp: the URL the server speaks Streamable HTTP on
--base-url <url>http and dav
--openapi <url|path>http: a URL, or a path resolved against the manifest's own directory
--operations <globs>http: which operations to expose, by operationId, path or tag
--service <kind>dav: caldav or carddav
--host, --portimap
--smtp-host, --smtp-portimap: omit the host and the mailbox is read-only, with no send capability
--root <path>, --exclude <names>fs
--header 'Name: value'sent on every request, repeatable; mcp and http only. Never Authorization
--auth-header <name>the header the credential is sent in
--auth-query <param>api-key only: the query parameter instead of a header
--scopes <list>oauth
--authorize-url, --token-urloauth: together or not at all; required on http
--client-app <name>which oauth_apps entry holds the client; defaults to the id
--registration <kind>dynamic or manual; mcp defaults to dynamic, everything else to manual
--authorize-param k=voauth: extra parameters on the authorization request, repeatable. Some vendors issue no refresh token without one
--redirect-uri <url>only for a vendor that matches the whole redirect URL (ADR-045)
--strategy <name>strategy: the name a provider in this build supplies, e.g. bunq. http only
--strategy-option k=vstrategy: repeatable, read by the strategy itself, for a sandbox host, say
--identity-url, --identity-fieldhttp: one GET that names the account, and which field to read
--setup-docs <url|text>where the credential comes from. A URL becomes a link, a sentence a step
--replace-manifestrewrite a declaration that exists and differs. --replace is about the credential

It also takes --id, --display-name, --label, --replace, --accept-broad-scopes and --non-interactive, which are forwarded to the connect that follows.

console
$ lanes link connect custom docs_server --connector mcp --auth oauth \
    --endpoint https://mcp.example.com/mcp --profile personal

$ lanes link connect custom thing --connector http --auth api-key --auth-header X-Api-Key \
    --base-url https://api.example.com/v1 --openapi https://api.example.com/openapi.json \
    --profile personal

$ lanes link connect custom mailbox --connector imap --auth basic \
    --host imap.example.com --smtp-host smtp.example.com --smtp-port 465 \
    --profile personal

--auth strategy names code a provider in this build carries, which a manifest of your own can borrow, and the only way to point a connection at a vendor's sandbox, since a built-in manifest's options are not yours to edit (ADR-046).

Thirteen of the thirty-five possible pairs are legal, and the rest are refused with the alternative named, an mcp connector has nowhere to put an API key, an imap one authenticates with a password, an fs one holds no account at all. Connectivity coverage is the whole matrix, including what no pair covers yet.

No flag carries a credential. The manifest declares what to ask for and the ordinary connect path asks, for the same reason secrets set reads stdin rather than an argument.

Two limits worth knowing. A declaration is written to the local filesystem, so this refuses a workspace that is a bucket: declare it where you deploy from, and lanes link deploy carries it up. And a failed connect leaves the manifest in place. That is the normal state of a hand-written one, status reports it, and the retry is plain lanes link connect <id>.

Reconcile, then serve every profile in the workspace on one endpoint.

--port <n>override the configured port
--onlyserve the resolved profile alone
console
$ lanes link start

The endpoint URL, whether it is answering, the profiles behind it, the token, and a registration command to adapt.

--showreveal the token
console
$ lanes link outputs --show

Let the Lanes dashboard read this workspace: its connections, its profiles, its audit log, and the data it holds. It mints a credential that reads all of that, then prints a link carrying it.

The credential is not read-only. It can create, edit and delete your memory, tasks, files, skills and entities, in every profile this workspace holds. It changes no connection, token, policy rule or configuration, and it never reads a vault value: those stay in the CLI. --rotate is how you take it back.

The token travels in the URL fragment, which is never sent to a server, so it does not reach a Lanes access log, a proxy, or a referrer header on the way to a page whose whole point is that Lanes does not see this data. The page fetches your endpoint directly.

--printprint the link for an existing pairing and change nothing
--rotatemint a fresh token, so whatever a browser is holding stops working
--yesdo not ask before installing mkcert

On this machine, a browser will not fetch http://127.0.0.1 from an https:// page; Safari refuses it outright and offers no way to allow it, so pairing installs a locally trusted certificate with mkcert and serves a second listener one port above the endpoint. That certificate goes into your system trust store, which is the largest thing any command here does, so it asks first and stops if you decline.

console
$ lanes link pair
ok    certificate installed

ok    the dashboard may now read https://127.0.0.1:7338

https://lanes.sh/dashboard/link#pair=llp_...&at=https%3A%2F%2F127.0.0.1%3A7338

The address rides in the link, not just the token. The read listener sits one port above instance.port, so an endpoint on anything but the default is on a port the dashboard has no way to guess, and a page that guessed wrong reported it unreachable while it was answering.

The endpoint has to be running for that link to read anything: lanes link start and lanes link pair are two commands, and pairing starts nothing.

On a deployed workspace there is no certificate to install; the platform terminates TLS with one browsers already trust, so nothing is installed and nothing is asked. The same two routes answer on the endpoint's own address, which is what the link carries in place of a loopback one (ADR-064).

console
$ lanes link pair --workspace cloud
ok    no certificate needed — this endpoint already has one a browser trusts

ok    the dashboard may now read https://<service>.run.app

A rotation on a deployed workspace takes up to five seconds to be refused, because the endpoint caches what it read rather than calling Secret Manager on every request.

The dashboard reads one endpoint at a time and can hold several pairings at once, so a laptop and a deployment both appear in the switcher and you pick which one you are looking at.

Open the Lanes desktop app on its Lanes Link page, at Settings → Integrations → Lanes Link, which drives this CLI from a window instead of a prompt. lanes link dashboard is the same command under its older name, from when it opened a page the endpoint served (ADR-053).

If the app is not there, it offers to install it with brew install --cask lanes-sh/lanes/lanes, then opens it. It asks first: this is the only command here that puts an application on the machine rather than a file in your own workspace. --yes answers ahead of time, and a run with no terminal is refused rather than assumed.

macOS only, because the app is. Elsewhere it says so and prints the link it would have opened.

--printprint the link instead of opening it
--yesinstall the app without asking, if it is missing

It takes neither --profile nor --workspace, and refuses them: it resolves nothing, and the app holds its own selection. If you have lanes link dashboard --profile … --workspace … in a script, drop the flags.

console
$ lanes link desktop
ok    opened Lanes → Settings → Integrations → Lanes Link
        needs Lanes 0.48.0 or newer; an older app ignores the link.

$ lanes link desktop --print
lanes://settings?page=integrations-link

The first run on a machine without it:

console
$ lanes link desktop
warn  the Lanes app is not installed
  brew install --cask lanes-sh/lanes/lanes
Install it now? [Y/n]
  installing, which takes a minute the first time…
ok    installed Lanes
ok    opened Lanes → Settings → Integrations → Lanes Link

Without Homebrew it cannot finish the job, so it hands over both routes: the line above, and https://lanes.sh/desktop to download the app directly.

Register this endpoint with an agent, and install the documents that describe it. With no argument, every harness found.

--name <name>register under a name other than lanes-link
--scope <scope>the harness's own scope; user by default
--token-env <VAR>the variable a harness reads its token from; LANES_LINK_TOKEN by default
--no-skillregister only, leaving the agent's files alone
--forcereplace a registration this name already has
--dry-runprint each command it would run, token redacted
console
$ lanes link mcp add
$ lanes link mcp add codex --no-skill

Where this endpoint is registered, and whether each harness's copy of the documents is current. Needs no profile and no workspace.

--name <name>look under a different registration
--scope <scope>look in a different scope
console
$ lanes link mcp list

Serve on stdin and stdout, for a client that spawns the command rather than calling a URL. Not typed by hand: it is what a config file names.

--onlyserve the resolved profile alone
console
$ lanes link mcp stdio

The bundled agent skill: its path, or the document itself.

--printwrite the document to stdout
--forcebroken, accepted and ignored
console
$ lanes link mcp skill --print

Names a --workspace. With no --profile it reports the whole workspace: every profile, whether it declares it, and how many connections each has, which is how a workspace declared by one profile and not its sibling becomes visible. With --profile it is the detailed view: connections, the capabilities reachable through them, and where the endpoint is.

No network call and no store opened either way, so it still answers for a workspace whose stores are unreachable, which is the case you most want an answer in.

console
$ lanes link status --workspace cloud                      # every profile
$ lanes link status   # one, in detail

Profiles

Write a new profile, and the workspace file if it is the first. Names no profile and no workspace of its own.

--workspace <name>a workspace per place it runs, repeatable; local is derived, others copied from a sibling
--non-interactivenever prompt
console
$ lanes link profile add personal

Every profile in the workspace and its path. Needs no profile and no workspace.

console
$ lanes link profile list

The profile, its own token, its config file, and the material it owns; its memory, tasks, assets, entities, skills and vault. Best effort: one refusal does not stop the rest, and anything that survived exits non-zero. The confirmation asks you to type the name.

The accounts outlive it. A connection belongs to the workspace and other profiles may grant it, so nothing here removes one; lanes link disconnect is that command, and it checks who else still needs the credential first.

--delete-datadelete the material this profile owns
--migrate-to <profile>move it into another profile instead
--dry-runprint the plan, write to nothing
--yesskip the confirmation
--workspace <name>which workspace the profile is in

One of --delete-data or --migrate-to is required, and --yes does not supply one. Everything else --yes skips is a confirmation of something the command was told to do; this is a question it has not been asked, and either default is a way to lose material or to put it somewhere nobody asked for. With a terminal it prompts; without one it refuses and names both spellings.

On a migration, a note, task, entity or asset the destination already holds is renamed, never overwritten; a colliding flight.md arrives as flight-2.md. Every rename is resolved before a byte moves, so you see it in the plan you confirm from.

console
$ lanes link profile remove work --dry-run --migrate-to personal

Removing profile work would delete:

  local
    object     profiles/work/lanes_memory/lan1/standup.md → profiles/personal/lanes_memory/lan1/standup.md
    object     profiles/work/lanes_memory/lan1/flight.md → profiles/personal/lanes_memory/lan1/flight-2.md — renamed
    file       profiles/work — the profile directory, once emptied

warn  lanes_memory/lan1/flight.md is already in "personal", so it arrives as lanes_memory/lan1/flight-2.md.

Two things cannot be renamed, and the command stops rather than guessing at either.

A skill both profiles have is refused, because a skill's name is not a filename: it becomes the capability id skills.<name>, which policy rules grant and MCP prompts are called by. One arriving as proc-b-2 would be granted by no rule the destination holds and offered to no client; and renaming only its directory is worse, because the frontmatter goes on declaring the old name, two skills claim one capability id, and skills list then refuses for the whole profile rather than for the one skill. Rename one of the two yourself, then run this again.

A sealed vault is refused for the same reason in a different form: two encrypted documents have no union, and a copy under another name is one no command opens. Move what you need across with lanes link vault list, then remove the profile with --delete-data.

Cursors are the one thing neither disposition asks about; they are derived, and adopting another profile's cursor would resume a mailbox from a position this profile never read. They go with the profile.

The positional name is what gets removed. --profile is refused, as it is on profile add: both name their profile positionally, so a flag naming a second one could only disagree with it.

Removed. It wrote default_profile, which nothing reads. Kept as a refusal for one release.

console
$ lanes link profile default work
error  lanes link profile default was removed.

Targets

A workspace holds connections and profiles, and names where they live: a credential store, a blob store, optionally a deployment. Everything above them is declared once and applies to all of them.

What this profile declares, and which one you named. Reads the file and asks nobody. Requires no workspace. It is the command you run to find out what to pass.

--urlsadd each deployable workspace's address, one platform lookup apiece
--workspace <name>mark one, and warn if it is not declared
console
$ lanes link workspace list

One workspace's adapters, its deployment, the address it answers on, and which release rolled it. Asks the platform for the address; the release comes off the registry, so it answers with the service scaled to zero.

--workspace <name>the workspace, if not given positionally
console
$ lanes link workspace show local

Removed. It wrote instance.default_target, which nothing read. Pass --workspace, or write a default with lanes set-workspace <name>, which every command that uses it echoes and every command that publishes or destroys refuses.

console
$ lanes link target use cloud
error  lanes link target use was removed.

Who you are

Declare one entry. kind is yours to choose; name, email, github are conventions. Order is the ranking. The first add also writes the connection row and the lanes_identity.* grant.

--note <text>when this entry applies, read by whatever picks between several of a kind
console
$ lanes link identity add name "A. Lovelace" --note "for anything published" \
    --profile personal

The block, and whether anything but the file can read it. Takes --profile only.

console
$ lanes link identity list --profile personal

Drop one entry, leaving the connection row and the grant in place.

console
$ lanes link identity remove name Ada --profile personal

Permissions

Default deny. A deny beats any allow whatever the order in the file, and the only operator beyond an exact name is a trailing .*.

The rules in force. Takes --profile only.

console
$ lanes link policy list --profile personal

Grant a capability, or a provider with gmail.*. A rule naming an unknown connection is refused at write time. Widening a vendor scope needs browser re-consent and goes through connect.

console
$ lanes link policy allow gmail.send_message --profile personal

Refuse a capability, whatever else allows it.

console
$ lanes link policy deny lanes_skills.manage.* --profile personal

The one bearer token this endpoint accepts, truncated unless asked otherwise.

--showreveal it
--rawprint only the token, no resolution line, for $(…)
console
$ lanes link token show --raw

Mint a new token; invalidates the old immediately. Every agent must be registered again, and a harness stores the token it was given, not the command that produced it.

--showreveal the replacement
--raw --yesbroken, accepted and ignored
console
$ lanes link token rotate --workspace local

Your own context

Memory, tasks, assets, skills, the vault and entities are providers holding your material rather than an account, so a profile arrives with all six declared and granted. There is nothing to connect (ADR-050). Each also has a CLI of its own, which reaches the same bytes an agent does.

Which store a thing goes in: memory is what is true, tasks is what is to be done, assets is a file, entities is who somebody is. Nothing refuses the wrong choice, so it is worth knowing: ADR-051 has the reasoning.

--connection <id> picks between several of a kind; with exactly one it is inferred.

Ids, titles, tags, and the date each was last written.

--connection <id>which memory connection
--tag <tag>only memories carrying this tag
--rawbroken, refused
console
$ lanes link memory list --profile personal

One entry's body, as stored.

console
$ lanes link memory get deploy-window --profile personal

Create or replace one entry; body on stdin. An entry is one Markdown file with YAML frontmatter, so a text editor is an equally good client.

--title <text>what a listing shows; a rewrite without one keeps the existing title
--connection <id>which memory connection
--tag <tag>broken, refused, though the help text lists it
console
$ printf 'The deploy window is Thursday evening.' \
    | lanes link memory write deploy-window --title "Deploy window" \
        --profile personal

Delete one entry, after printing it and asking.

Broken in a script. With no terminal it says "Pass --yes to proceed", and --yes is refused. Interactive use is unaffected.

console
$ lanes link memory forget deploy-window --profile personal

What is outstanding: id, status, title, due date, tags. Shows in_progress, open and blocked and hides the rest, because the question is almost always what is left to do.

--connection <id>which tasks connection
--status <status>one status, or all for everything
--tag <tag>only tasks carrying this tag
console
$ lanes link tasks list --profile personal

Tasks (2 outstanding)
  write-the-release-notes  in_progress  write the release notes
  chase-the-invoice        open         chase the invoice        due 2026-09-01  billing

One task: title, status, due date, tags, then the notes.

console
$ lanes link tasks get chase-the-invoice --profile personal

Record something to be done. The title is the argument, because a task is usually one line; notes are optional and read from stdin when there are any. A task is one Markdown file with YAML frontmatter, so a text editor is an equally good client.

--status <status>in_progress, open, blocked, muted, done, dropped. Defaults to open.
--due <when>as you would write it: 2026-09-01, or a full instant. Kept verbatim.
--tag <tag>one label
console
$ lanes link tasks add "chase the invoice" --due 2026-09-01 --profile personal
ok    added task chase-the-invoice

$ printf 'Third reminder.' | lanes link tasks add "chase the invoice" --profile personal

The id is derived from the title. Adding under an id that exists replaces it, keeping the date it was first recorded.

Change a task in place. This is how a task is closed, and the record of having done it is the useful half, and it is what stops the same thing being suggested next week. Omitted flags leave their fields alone.

--status <status>the new status
--title <t>replaces the title
--due <when>replaces the due date; --due "" clears it
--tag <tag>replaces the tags
console
$ lanes link tasks update chase-the-invoice --status done --profile personal
ok    updated task chase-the-invoice — now done

Delete a task, after printing it and asking. For something finished or decided against, prefer update --status done or --status dropped: deleting loses the record that it happened.

--yesskip the confirmation
console
$ lanes link tasks remove mistake --yes --profile personal

Every file kept in this profile, newest first, with its type and size. This is the whole index, and an asset carries no description, so what a file is for belongs in memory beside its name.

console
$ lanes link assets list --profile personal

Assets (2)
  blob.bin    application/octet-stream  3 KB  2026-08-27
  report.csv  text/csv                  12 B  2026-08-27

The bytes, to stdout. Redirect them. It refuses a terminal rather than writing a binary into your scrollback.

console
$ lanes link assets get invoice.pdf --profile personal > invoice.pdf

This is the one place the CLI and the capability deliberately differ: lanes_assets.get over MCP describes a binary rather than returning it, because a conversation has a context window and a shell does not.

Keep a file. A path on this machine; the other four sources the capability takes exist because the endpoint may not be where the caller is, and a CLI already is.

--name <n>what to call it, where the path's basename is wrong
--content-type <t>for the file whose extension does not say what it is
console
$ lanes link assets add ~/Downloads/invoice.pdf --profile personal
ok    kept invoice.pdf — 184 KB, sha256 9284ed4fd7fe…

Storing under a name that exists replaces it. A name may not contain a path separator, start with a dot, or end .meta or .tmp. The last two are suffixes a blob store keeps its own bookkeeping under and would hide the file.

Delete a file, after printing it and asking. There is no trash.

--yesskip the confirmation
console
$ lanes link assets remove invoice.pdf --yes --profile personal

The procedures agents can invoke, and each description.

--connection <id>which skills connection
--rawbroken, refused
console
$ lanes link skills list --profile personal

One skill exactly as stored, frontmatter included.

console
$ lanes link skills show review-diff --profile personal

Create or replace one skill. A whole Markdown document: YAML frontmatter carrying a description, then the body, where {{argument}} is substituted at invocation. The --- fence is required.

--file <path>read the document from a file rather than stdin
--connection <id>which skills connection
console
$ lanes link skills add review-diff --file review-diff.md --profile personal

Becomes the MCP prompt skills_<name>; a running endpoint picks it up within seconds.

Delete one skill, after asking.

Broken in a script, same as memory forget: it asks for --yes, and refuses --yes.

console
$ lanes link skills remove review-diff --profile personal

Where this profile's memory and skills are kept, and how many of each.

console
$ lanes link knowledge show --profile personal

Keep memory and skills in a private GitHub repository, over the API, or bring them back onto the workspace's own storage. The block is written into every workspace the profile declares; each still needs the token in its own store, which is what secrets push is for (ADR-041).

--repo <owner/name>the repository
--branch <name>the branch
--path <prefix>a subdirectory rather than the root
--migratemove what is already stored, in one commit
--no-migrateswitch and leave it where it is
--keepmove it, and leave the local copies in place, unread
--allow-publicproceed against a public repository, otherwise refused
--replaceask for the token again
--yesskip the confirmations
console
$ lanes link knowledge use github --repo my-org/my-notes --migrate \
    --profile personal
$ lanes link knowledge use local --migrate --profile personal

Names and descriptions, never values.

--connection <id>which vault connection
console
$ lanes link vault list --profile personal

One item, truncated to a few characters and a length.

Broken. --show and --raw are both refused, so there is no way to reveal a value. The output tells you to pass --show.

console
$ lanes link vault get github_token --profile personal
pla…  (11 chars — --show to reveal)

Store one item; value on stdin. Not readable over MCP until the endpoint restarts, and it needs a lanes_vault.get.<id> grant.

--description <text>broken, refused, though the help text lists it
console
$ printf %s "$SOME_TOKEN" | lanes link vault set github_token --profile personal

Delete one item, after asking. The value cannot be recovered.

Broken in a script. Asks for --yes, and refuses --yes.

console
$ lanes link vault remove github_token --profile personal

Mint a LANES_LINK_VAULT_KEY and print it once. Written nowhere, because a key beside the ciphertext it protects nothing. Needs no profile and no workspace, and refuses both.

console
$ lanes link vault key generate

Look somebody or something up. Every criterion is combined, and a bare lanes link entities lists everything. More than one match is a normal answer, and the order is not a ranking.

--type <type>only this type: person, company, whatever you have used
--tag <tag>only entities carrying this tag
--attr <kind>[=<value>]has this attribute; repeatable, and --attr github means "has one at all"
--related <predicate>=<id>points at this entity; repeatable
--connection <id>which entities connection
console
$ lanes link entities find Jan --profile personal
$ lanes link entities find --related works_at=acme-bv --profile personal

One entity in full, with its relationships and everything pointing at it. An edge to something not declared yet is shown as such rather than hidden.

console
$ lanes link entities get acme-bv --profile personal

Declare one, or update one that exists. A flag you leave out keeps what is stored, so this is how you correct one field without restating the rest. Notes come from stdin and are optional.

--name <id>the id, where the one derived from the name is not what you want
--type <type>person, company, project, free-form
--alias <name>another name it is known by; repeatable
--attr <kind>=<value>an address or handle; repeatable, most-preferred first
--related <predicate>=<id>an edge from this entity; repeatable, replaces the existing set
--tag <tag>a label for filtering
console
$ printf %s "Prefers email over calls." | lanes link entities write "Jan Bakker" \
    --type person --alias Jan --attr [email protected] --related works_at=acme-bv \
    --profile personal

Add one edge, leaving everything else alone. It is written only on <from>, and the reverse direction is derived, so do not write it the other way as well. The other end need not exist yet, and you are told when it does not.

console
$ lanes link entities link jan-bakker knows=marta-silva --profile personal

Remove one, after printing it and asking. Edges pointing at it from elsewhere are not rewritten, so it names them first.

--yesskip the confirmation

Rebuild the lookup index from the files. It self-heals on the next write, so this is for when a write is not coming: a bulk edit made in an editor, or a pull from a repository. It says whether it had to do anything, and why.

console
$ lanes link entities reindex --profile personal

Deploying

Set up what is missing, build the image, roll one revision, print the URL. Names a --workspace and sends every profile that declares it, because that is the set the endpoint will open. See Deploying to Cloud Run.

--profile <name>only these, repeatable; the first owns the endpoint's token
--dry-runprint every platform command, run none
--access iam|publicwho gets past the platform's own door
--iamolder spelling of --access iam
--service-account <name>the identity the revision runs as
--tag <name>the image tag; a UTC timestamp by default
--yesskip the confirmations
--non-interactivetake the stored answers, never prompt
console
$ lanes link deploy --workspace cloud --dry-run                     # every profile declaring it
$ lanes link deploy --workspace cloud --profile personal --dry-run  # only this one

A first deploy is the exception: a workspace no profile declares yet has no set to derive, so name the profile it belongs to and deploy creates the workspace in it.

It refuses two things rather than deciding them. Which profile owns the endpoint's token, when more than one declares the workspace and no previous deploy recorded an answer. One token reaches every profile behind it. And a deploy where two profiles would write the same flat credential reference into the one store the workspace has, which would leave one of them reading the other's account.

Reconcile the workspace with the copy a deployment reads. Names a --workspace; --profile narrows it to one.

Anything one side is missing is copied to it; anything both sides hold differently stops the run and prints the difference. Credentials, state and the audit log are never copied, only config, skills and provider manifests, which is exactly what a deploy uploads.

--from gs://<bucket>where the deployment's copy lives, when nothing here says
--discoverask the platform to find it, the only route that works from nothing
--prefer local|remotewhich side wins where both disagree
--dry-runprint the differences, write to neither side
console
$ lanes link sync workspaces --workspace cloud --dry-run
$ lanes link sync workspaces --workspace cloud
personal
  ← workspaces.cloud          missing locally
  ← connections.gmail.work    missing locally

This is how a workspace lost from a profile comes back: the deployment is still running and its bucket still holds the profile as the last deploy left it. Where to look is tried cheapest first: a workspace you still declare, then the deployments: index deploy writes into workspaces.yaml, then --from, then --discover.

connect comes after the first deploy, because a store that does not exist yet is not somewhere to write a credential, and a second deploy is what gets a revision to see those accounts.

Credential reference names in this workspace. No command here prints a value.

console
$ lanes link secrets list

Store one credential value, read from stdin.

console
$ printf %s "[email protected]:xxxx-xxxx-xxxx-xxxx" \
    | lanes link secrets set icloud/ada

Copy credential values between two workspaces' stores. Takes --profile only, because --from and --to name the workspaces, so --workspace is refused.

--from <workspace>source
--to <workspace>destination
--overwritereplace a reference the destination already holds
--dry-runprint what would be copied
console
$ lanes link secrets push --from local --to cloud

Copies and never deletes, and skips what the destination has unless --overwrite, because the deployed copy may be newer.


Inspection

Cheapest first: check (static), doctor (read-only external), plan (what would change), then start or deploy.

Static validation: contract major, no credential values in config, referential integrity, workspaces resolvable. No external call. Takes --profile only.

console
$ lanes link check --profile personal

Credentials resolve, stores reachable, token present, state agrees with config. Also reports capabilities the upstream has grown since you connected. Exits non-zero on a finding, each carrying the command that fixes it.

console
$ lanes link doctor --profile personal

--fix applies the one repair doctor can make on its own: a provider this project renamed under you, where the profile still names the old id. That refusal happens at config load, so it takes every other command down with it. doctor is the one that still answers, and it moves the connection row, its policy rules and the stored credential together. Everything else doctor reports is a decision only you can make, and --fix does not touch it.

console
$ lanes link doctor --fix --profile personal

Without --fix it prints what it would move and writes nothing.

What reconcile would change, and nothing else. Reconcile disables undeclared connections, which should never be a surprise.

console
$ lanes link plan --profile personal

What the endpoint would hand a client right now, asked over the wire: one initialize, one tools/list. Not derived from config, which is the point. Reports refused and unreachable as different problems.

console
$ lanes link tools

The log the dispatcher writes, allowed and denied both, arguments redacted per provider.

--limit <n>how many entries
--denied-onlythe interesting half
--format mda Markdown table
console
$ lanes link audit tail --limit 25 --denied-only

Whether anything in the log has been altered or removed. Each record carries the hash of the one before it, so an edit shows as a mismatch, a removal as a sequence gap, and a truncated run as a count that disagrees with its marker. Exits non-zero on a break.

--limit <n> --format <f>broken, accepted and ignored; it walks every chain regardless
console
$ lanes link audit verify

The resolved config as JSON. Takes --profile only.

console
$ lanes link config show --profile personal

Which release this is. Same as lanes --version. Needs no profile and no workspace.

console
$ lanes link version

Install the newer release. There is no build step, so an update replaces the installed package directory. Bun only; from a checkout it refuses and says git pull is the update there. Needs no profile and no workspace.

--checkreport only, exiting non-zero when a newer release exists
console
$ lanes link update --check

Attachments

Stage a file and print the handle a send can name it by. A path attachment names the filesystem the endpoint sees, which stops meaning anything once the endpoint is not where the file is; a handle does not care.

--connection <provider>.<account>which connection the attachment belongs to
console
$ lanes link attach ./report.pdf --connection gmail.con1 --profile personal

Aliases

The older spelling of mcp skill, from before lanes link skills existed. Kept working, unadvertised.

--printwrite the document to stdout
--forcebroken, accepted and ignored
console
$ lanes link skill --print

The command list at a glance. Also lanes link --help, and --help on any command.

console
$ lanes link help

Selection

Workspace root: LANES_LINK_HOME, then the nearest ancestor holding workspaces.yaml, then ~/.lanes-link.

Profile and workspace: --profile and --workspace, and nothing else. No variable, no config key, no default (ADR-037). A command naming neither refuses and lists what there is to choose from. instance.default_target and default_profile are still parsed and no longer read, so check and doctor can tell you the line is inert.

Refuse --workspace: check, config show, policy list, identity list, secrets push.

Require no workspace: workspace list.

Name neither: profile add, profile list, profile default, workspace use, mcp list, mcp skill, skill, vault key generate, version, update, help.