---
name: pp-tailscale
description: "Tailscale admin from the terminal, with safe route and policy edits an agent can plan before it writes. Trigger phrases: `approve the exit node`, `check tailscale key expiry`, `add a tailscale policy entry`, `who has this machine shared`, `who can reach the nas on port 445`, `use tailscale`, `run tailscale-pp-cli`."
author: "Cathryn Lavery"
license: "Apache-2.0"
argument-hint: "<command> [args] | install cli|mcp"
allowed-tools: "Read Bash"
metadata:
  openclaw:
    requires:
      bins:
        - tailscale-pp-cli
    install:
      - kind: go
        bins: [tailscale-pp-cli]
        module: github.com/mvanhorn/printing-press-library/library/cloud/tailscale/cmd/tailscale-pp-cli
---

# Tailscale — Printing Press CLI

## Prerequisites: Install the CLI

This skill drives the `tailscale-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first:

1. Install via the Printing Press installer. It defaults binaries to `$HOME/.local/bin` on macOS/Linux and `%LOCALAPPDATA%\Programs\PrintingPress\bin` on Windows:
   ```bash
   npx -y @mvanhorn/printing-press-library install tailscale --cli-only
   ```
2. Verify: `tailscale-pp-cli --version`
3. Ensure the reported install directory is on `$PATH` for the agent/runtime that will invoke this skill.

If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.6 or newer). This installs into `$GOPATH/bin` (default `$HOME/go/bin`), so add that directory to `$PATH` instead:

```bash
go install github.com/mvanhorn/printing-press-library/library/cloud/tailscale/cmd/tailscale-pp-cli@latest
```

If `--version` reports "command not found" after install, the runtime cannot see the binary directory on `$PATH`. Do not proceed with skill commands until verification succeeds.

Nearly every Tailscale admin API endpoint (tailnet deletion is deliberately left out), plus commands that make the two dangerous whole-object writes safe: routes approve keeps the routes a device already has, and policy add-entry keeps comments, backs up, validates, diffs, and writes with an ETag check. Those safe-write commands (routes approve and unapprove, shares revoke, policy add-entry and restore) write only with --yes and are plan-only as MCP tools; in --agent mode every other mutation is a dry run unless --yes. devices expiry, routes overview, and shares audit answer fleet questions the admin console shows one machine at a time, and access check and devices inspect give an agent one target's full picture before it acts.

## When to Use This CLI

Use this CLI when an agent or operator needs to administer a tailnet: approve routes and exit nodes, edit the policy file, audit or revoke machine shares, check key expiry, or ask which rules allow a connection. It is the right choice when a change must not clobber existing routes or policy content. The device selector self means this machine and needs the local tailscale command; otherwise pass a hostname, MagicDNS name, Tailscale IP, or nodeId.

## Anti-triggers

Do not use this CLI for:
- Connecting this machine to a tailnet or changing its local settings; use the local tailscale command (tailscale up, tailscale set)
- Sending files with Taildrop or serving a local port; use tailscale file and tailscale serve
- Managing Headscale or other self-hosted control servers

## Unique Capabilities

These capabilities aren't available in any other tool for this API.

### Safe writes to whole-object endpoints
- **`routes approve`** — Approve a subnet route or exit node on one device without dropping the routes it already has.

  _Reach for this instead of the raw routes endpoint whenever an agent is asked to enable an exit node or subnet route._

  ```bash
  tailscale-pp-cli routes approve self --all-advertised
  ```
- **`routes unapprove`** — Remove approval for one route or the exit-node pair while leaving every other approved route in place.

  _Use it to turn off an exit node without breaking the subnet routes on the same machine._

  ```bash
  tailscale-pp-cli routes unapprove self --exit-node
  ```
- **`policy add-entry`** — Append one nodeAttrs, grants, acls, or ssh entry to the policy file with comments preserved, a local backup, validation, a diff, and a concurrency check.

  _Use it whenever an agent needs to add Taildrive, Funnel, or access grants without rewriting the whole policy file._

  ```bash
  tailscale-pp-cli policy add-entry nodeAttrs --entry '{"target":["autogroup:member"],"attr":["drive:access"]}' --dry-run
  ```
- **`policy restore`** — Roll the policy file back to a local backup after validating it and showing the diff.

  _This is the undo for every policy write the CLI makes._

  ```bash
  tailscale-pp-cli policy restore --list
  ```

### Tailnet state an agent can reason about
- **`routes overview`** — See approved, pending, and stale routes and exit-node state for every device in one table.

  _Run it before approving anything to see what is actually waiting._

  ```bash
  tailscale-pp-cli routes overview --pending
  ```
- **`devices expiry`** — List every device by days until its key expires and flag the ones inside a window.

  _Use it as a weekly check or a CI gate so nobody gets logged out by surprise._

  ```bash
  tailscale-pp-cli devices expiry --within 14
  ```
- **`shares audit`** — List every machine-share invite across the tailnet with who accepted it and which invites are still redeemable.

  _Answer who has access to a shared machine in one call._

  ```bash
  tailscale-pp-cli shares audit --pending
  ```
- **`shares revoke`** — Revoke machine-share invites by ID or by device and acceptor, with a plan before any delete.

  _Use it to revoke a contractor's share invites without hunting IDs device by device, then confirm in the admin console that an accepted share is gone._

  ```bash
  tailscale-pp-cli shares revoke --device self --pending
  ```

### Agent-native networking
- **`access check`** — Show which policy rules let a user reach a device and port, against the live policy or a candidate file.

  _Check access before and after a policy change without guessing at rule evaluation._

  ```bash
  tailscale-pp-cli access check --to self:22
  ```
- **`devices inspect`** — Get one device's identity, routes, key expiry, shares, and recent changes in a single record.

  _Make this the first call in any task about a specific machine._

  ```bash
  tailscale-pp-cli devices inspect self --agent
  ```

## Command Reference

Every Tailscale admin API endpoint except tailnet deletion, grouped by resource. Writes (POST/PUT/PATCH/DELETE) run as dry runs under `--agent` unless `--yes` is passed. Add `--help` to any command for its flags.

**device**

- `tailscale-pp-cli device attributes delete <deviceId> <attributeKey>` (DELETE): Delete a posture attribute from the specified device.
- `tailscale-pp-cli device attributes list <deviceId>` (GET): Retrieve all posture attributes for the specified device.
- `tailscale-pp-cli device attributes set <deviceId> <attributeKey>` (POST): Create or update a custom posture attribute on the specified device.
- `tailscale-pp-cli device authorized set <deviceId>` (POST): This call marks a device as authorized or revokes its authorization for tailnets where device authorization is required.
- `tailscale-pp-cli device delete <deviceId>` (DELETE): Deletes the device from its tailnet.
- `tailscale-pp-cli device device-invites create <deviceId>` (POST): Create new share invites for a device.
- `tailscale-pp-cli device device-invites list <deviceId>` (GET): List all share invites for a device. OAuth Scope: `device_invites:read`.
- `tailscale-pp-cli device expire key <deviceId>` (POST): Mark a device's node key as expired.
- `tailscale-pp-cli device get <deviceId>` (GET): Retrieve the details for the specified device. OAuth Scope: `devices:core:read`.
- `tailscale-pp-cli device ip set <deviceId>` (POST): When a device is added to a tailnet, its Tailscale IPv4 address is set at random either from the CGNAT range.
- `tailscale-pp-cli device key set <deviceId>` (POST): When a device is added to a tailnet, its key expiry is set according to the tailnet's key expiry setting.
- `tailscale-pp-cli device name set <deviceId>` (POST): When a device is added to a tailnet, its Tailscale device name.
- `tailscale-pp-cli device routes list <deviceId>` (GET): Retrieve the list of subnet routes that a device is advertising, as well as those that are enabled for it.
- `tailscale-pp-cli device routes set <deviceId>` (POST): Set a device's enabled subnet routes by replacing the existing list of subnet routes with the supplied parameters.
- `tailscale-pp-cli device tags set <deviceId>` (POST): Tags let you assign an identity to a device that is separate from human users.

**device-invites**

- `tailscale-pp-cli device-invites accept` (POST): Accepts the invitation to share a device into the requesting user's tailnet.
- `tailscale-pp-cli device-invites delete <deviceInviteId>` (DELETE): Delete a specific device invite. OAuth Scope: `device_invites`.
- `tailscale-pp-cli device-invites get <deviceInviteId>` (GET): Retrieve a specific device invite. OAuth Scope: `device_invites:read`.
- `tailscale-pp-cli device-invites resend device-invite <deviceInviteId>` (POST): Resend a device invite by email.

**organizations**

- `tailscale-pp-cli organizations tailnets create <organization>` (POST): Create an API-only tailnet in the organization.
- `tailscale-pp-cli organizations tailnets list <organization>` (GET): List all tailnets in the organization, including the original tailnet and any API-only tailnets.

**posture**

- `tailscale-pp-cli posture delete <id>` (DELETE): Delete a specific posture integration. OAuth Scope: `feature_settings`.
- `tailscale-pp-cli posture get <id>` (GET): Gets the posture integration identified by `{id}`. OAuth Scope: `feature_settings:read`.
- `tailscale-pp-cli posture update <id>` (PATCH): Updates the posture integration identified by `{id}`.

**tailnet**

- `tailscale-pp-cli tailnet acl get` (GET): Retrieves the current policy file for the given tailnet.
- `tailscale-pp-cli tailnet acl preview` (POST): When given a user or IP port to match against, returns the tailnet policy rules that apply to that resource.
- `tailscale-pp-cli tailnet acl set` (POST): Sets the ACL for the given tailnet.
- `tailscale-pp-cli tailnet acl validate` (POST): This endpoint works in one of two modes, neither of which modifies your current tailnet policy file: - Run ACL tests.
- `tailscale-pp-cli tailnet aws-external-id create-or-get` (POST): Get an AWS external id to use for streaming tailnet logs to S3 using role-based authentication.
- `tailscale-pp-cli tailnet aws-external-id validate <id>` (POST): Validate that Tailscale can assume your IAM role with (and only with) this external ID. OAuth Scope: `log_streaming`.
- `tailscale-pp-cli tailnet contacts get` (GET): Retrieve the tailnet's current contacts. OAuth Scope: `account_settings:read`.
- `tailscale-pp-cli tailnet contacts resend-verification-email` (POST): Resends the verification email for this contact, if and only if verification is still pending.
- `tailscale-pp-cli tailnet contacts update` (PATCH): Update the preferences for this type of contact.
- `tailscale-pp-cli tailnet device-attributes batch-update` (PATCH): Batch updates posture attributes across devices in a tailnet.
- `tailscale-pp-cli tailnet devices list` (GET): Lists the devices in a tailnet. OAuth Scope: `devices:core:read`.
- `tailscale-pp-cli tailnet dns get-configuration` (GET): Retrieves the full DNS configuration for a tailnet, including global nameservers, split DNS routes, search paths.
- `tailscale-pp-cli tailnet dns get-preferences` (GET): Retrieves the DNS preferences that are currently set for the given tailnet.
- `tailscale-pp-cli tailnet dns get-split` (GET): Retrieves the split DNS settings, which is a map from domains to lists of nameservers.
- `tailscale-pp-cli tailnet dns list-nameservers` (GET): Lists the global DNS nameservers for a tailnet.
- `tailscale-pp-cli tailnet dns list-search-paths` (GET): Retrieves the list of search paths, also referred to as *search domains*, that is currently set for the given tailnet.
- `tailscale-pp-cli tailnet dns set-configuration` (POST): Replaces the DNS configuration for the given tailnet.
- `tailscale-pp-cli tailnet dns set-nameservers` (POST): Replaces the list of global DNS nameservers for the given tailnet with the list supplied in the request.
- `tailscale-pp-cli tailnet dns set-preferences` (POST): Set the DNS preferences for a tailnet; specifically, the MagicDNS setting.
- `tailscale-pp-cli tailnet dns set-search-paths` (POST): Replaces the list of search paths for the given tailnet.
- `tailscale-pp-cli tailnet dns set-split` (PUT): Replaces the split DNS settings for a given tailnet.
- `tailscale-pp-cli tailnet dns update-split` (PATCH): Performs partial updates of the split DNS settings for a given tailnet.
- `tailscale-pp-cli tailnet keys create` (POST): Creates a new auth key.
- `tailscale-pp-cli tailnet keys delete <keyId>` (DELETE): Deletes a specific api access token or auth key.
- `tailscale-pp-cli tailnet keys get <keyId>` (GET): Returns a JSON object with information about a specific api access token, OAuth client, federated identity, or auth key.
- `tailscale-pp-cli tailnet keys list` (GET): Returns a list of active auth keys, API access tokens and trust credentials.
- `tailscale-pp-cli tailnet keys set <keyId>` (PUT): Set the configuration for an existing OAuth client or federated identity.
- `tailscale-pp-cli tailnet logging disable-log-streaming` (DELETE): Delete the log streaming configuration for the provided log type. OAuth Scope: `log_streaming`.
- `tailscale-pp-cli tailnet logging get-log-streaming-configuration` (GET): Retrieve the log streaming configuration for the provided log type. OAuth Scope: `log_streaming:read`.
- `tailscale-pp-cli tailnet logging get-log-streaming-status` (GET): Retrieve the log streaming status for the provided log type. OAuth Scope: `log_streaming:read`.
- `tailscale-pp-cli tailnet logging list-configuration-audit-logs` (GET): List all configuration audit logs for a tailnet. OAuth Scope: `logs:configuration:read`.
- `tailscale-pp-cli tailnet logging list-network-flow-logs` (GET): List all network flow logs for a tailnet. OAuth Scope: `logs:network:read`.
- `tailscale-pp-cli tailnet logging set-log-streaming-configuration` (PUT): Set the log streaming configuration for the provided log type. OAuth Scope: `log_streaming`.
- `tailscale-pp-cli tailnet oauth-apps create` (POST): Create an OAuth app within a tailnet.
- `tailscale-pp-cli tailnet oauth-apps delete <appId>` (DELETE): Delete a specific OAuth app. OAuth Scope: `oauth_apps`.
- `tailscale-pp-cli tailnet oauth-apps get <appId>` (GET): Retrieve a specific OAuth app. OAuth Scope: `oauth_apps:read`.
- `tailscale-pp-cli tailnet oauth-apps list` (GET): List all OAuth apps for a tailnet. OAuth Scope: `oauth_apps:read`.
- `tailscale-pp-cli tailnet oauth-apps update <appId>` (PUT): Update a specific OAuth app.
- `tailscale-pp-cli tailnet posture create` (POST): Create a posture integration, returning the resulting PostureIntegration.
- `tailscale-pp-cli tailnet posture list` (GET): List all of the posture integrations for a tailnet. OAuth Scope: `feature_settings:read`.
- `tailscale-pp-cli tailnet services delete <serviceName>` (DELETE): Delete the specified Service from the tailnet. OAuth Scope: `services`.
- `tailscale-pp-cli tailnet services get <serviceName>` (GET): Retrieve the details for the specified Service. OAuth Scope: `services:read`.
- `tailscale-pp-cli tailnet services get-device-approval <serviceName> <deviceId>` (GET): Retrieve the approval status of the specified Service on a specific device. OAuth Scope: `services`.
- `tailscale-pp-cli tailnet services list` (GET): List all Services configured for the tailnet.
- `tailscale-pp-cli tailnet services list-hosts <serviceName>` (GET): List all devices that are hosting the specified Service. OAuth Scope: `services`.
- `tailscale-pp-cli tailnet services update <serviceName>` (PUT): Update or create the specified Service.
- `tailscale-pp-cli tailnet services update-device-approval <serviceName> <deviceId>` (POST): Update the approval status of the specified Service on a specific device. OAuth Scope: `services`.
- `tailscale-pp-cli tailnet settings get` (GET): Retrieve the settings for a specific tailnet.
- `tailscale-pp-cli tailnet settings update` (PATCH): Update the settings for a specific tailnet.
- `tailscale-pp-cli tailnet user-invites create` (POST): Create, and optionally email out, new user invites to join the tailnet.
- `tailscale-pp-cli tailnet user-invites list` (GET): List all open (not yet accepted) user invites to the tailnet.
- `tailscale-pp-cli tailnet users list` (GET): List all users of a tailnet. OAuth Scope: `users:read`.
- `tailscale-pp-cli tailnet webhooks create` (POST): Create a webhook within a tailnet. OAuth Scope: `webhooks`.
- `tailscale-pp-cli tailnet webhooks list` (GET): List all webhooks for a tailnet. OAuth Scope: `webhooks:read`.

**user-invites**

- `tailscale-pp-cli user-invites delete <userInviteId>` (DELETE): Deletes a specific user invite.
- `tailscale-pp-cli user-invites get <userInviteId>` (GET): Retrieve a specific user invite.
- `tailscale-pp-cli user-invites resend user-invite <userInviteId>` (POST): Resend a user invite by email.

**users**

- `tailscale-pp-cli users <userId>` (GET): Retrieve details about the specified user. OAuth Scope: `users:read`.
- `tailscale-pp-cli users approve user <userId>` (POST): Approve a pending user's access to the tailnet.
- `tailscale-pp-cli users delete user <userId>` (POST): Delete a user from their tailnet.
- `tailscale-pp-cli users restore user <userId>` (POST): Restores a suspended user's access to their tailnet.
- `tailscale-pp-cli users role set <userId>` (POST): Update the role for the specified user. OAuth Scope: `users`.
- `tailscale-pp-cli users suspend user <userId>` (POST): Suspends a user from their tailnet.

**webhooks**

- `tailscale-pp-cli webhooks delete <endpointId>` (DELETE): Delete a specific webhook. OAuth Scope: `webhooks`.
- `tailscale-pp-cli webhooks get <endpointId>` (GET): Retrieve a specific webhook. OAuth Scope: `webhooks:read`.
- `tailscale-pp-cli webhooks rotate webhook-secret <endpointId>` (POST): Rotate and generate a new secret for a specific webhook.
- `tailscale-pp-cli webhooks test webhook <endpointId>` (POST): Test a specific webhook by sending out a test event to the endpoint URL.
- `tailscale-pp-cli webhooks update <endpointId>` (PATCH): Update a specific webhook. OAuth Scope: `webhooks`.

### Finding the right command

When you know what you want to do but not which command does it, ask the CLI directly:

```bash
tailscale-pp-cli which "approve an exit node"
```

`which` resolves a natural-language capability query to the best matching command from this CLI's curated feature index. Exit code `0` means at least one match; exit code `2` means no confident match — fall back to `--help` or use a narrower query. `--json` (and other machine formats) keep that exit-2 contract and write `{"matches":[]}` on stdout so agents can inspect the envelope without treating a miss as success.

## Recipes

### Turn a Mac into an exit node safely

```bash
tailscale-pp-cli routes approve home-mac --exit-node --dry-run
```

Prints the before and after enabled routes; rerun with --yes to apply.

### Give members Taildrive access

```bash
tailscale-pp-cli policy add-entry nodeAttrs --entry '{"target":["autogroup:member"],"attr":["drive:access"]}' --dry-run
```

Shows the diff and validation result; rerun with --yes to write with the ETag check.

### Expiry report for agents

```bash
tailscale-pp-cli devices expiry --within 30 --agent --select items.machi