kcd is both the daemon and the command-line client. Sub-commands that interact with a running daemon communicate over a Unix socket (/run/user/$UID/kcd/kcd.sock). The daemon itself is started with kcd daemon.
These flags apply to every command:
| Flag | Default | Description |
|---|---|---|
--config <path> |
~/.config/kcd/kcd.toml |
Path to config file |
--log-level <level> |
info |
Log verbosity: debug, info, warn, error, quiet |
--help, -h |
— | Show help |
--version, -v |
— | Show version, commit, and build date |
--config and --log-level can also be set via environment variables KCD_CONFIG and KCD_LOG_LEVEL.
Start the kcd background daemon. This is the only command that does not connect to a running daemon — it is the daemon.
kcd daemon [--config <path>] [--log-level <level>]
The daemon:
- Listens for device announcements via UDP and mDNS (always on)
- Broadcasts its own identity only during
kcd pair(listen mode) - Accepts inbound TCP connections on port 1716
- Runs all enabled plugins
- Opens the IPC Unix socket for CLI clients
- Sends SIGINT or SIGTERM to stop cleanly
Running as a systemd user service (recommended)
cp packaging/kcd-user.service ~/.config/systemd/user/kcd.service
systemctl --user daemon-reload
systemctl --user enable --now kcdCheck status:
systemctl --user status kcd
journalctl --user -u kcd -fCheck runtime dependencies and print a coloured pass/fail table. Exits with code 1 if any check fails. Works even when the daemon is not running (the daemon-running check will show as failed).
kcd doctor
Checks performed:
- Daemon reachable via IPC socket
notify-sendinstalled (desktop notifications)wl-copyorxclipinstalled (clipboard sync)sshfsinstalled (SFTP mount)ydotoolorxdotoolinstalled (mousepad)- Port 1716/UDP available
- Port 1716/TCP available
- Config file readable
- TLS cert file exists
Show daemon runtime information.
kcd status [--json]
Flags
| Flag | Description |
|---|---|
--json |
Output raw JSON |
Example output
kcd v1.0.5 — up 3h 12m
Socket: /run/user/1000/kcd/kcd.sock
Config: /home/user/.config/kcd/kcd.toml
Devices: 2 known, 1 connected
Plugins: Battery, Clipboard, Notification, Share, ...
List all known devices — both currently connected and remembered from previous sessions.
kcd devices [--json]
Flags
| Flag | Description |
|---|---|
--json |
Output as a JSON array |
--watch, -w |
Stream device changes live (clears screen on each change) |
Example output
DEVICE ID NAME TYPE STATE CONNECTED
---------------------------------------------------------------------------------------------------
a1b2c3d4_e5f6_7890_abcd_ef1234567890 Pixel 8 Pro phone Paired true
b9e1f234_0000_1111_2222_333344445555 Galaxy Tab S9 tablet Unpaired false
JSON output
kcd devices --json | jq '.[0].ID'[
{
"ID": "a1b2c3d4_e5f6_7890_abcd_ef1234567890",
"Name": "Pixel 8 Pro",
"Type": "phone",
"State": "Paired",
"Connected": true
}
]Example — live mode
kcd devices --watch
Manually connect to a device by IP address. Use this when UDP broadcast and mDNS are blocked (corporate Wi-Fi, Docker, university networks).
kcd connect <ip-address>
Example
kcd connect 192.168.1.50On the Android side, open KDE Connect → ⋮ → "Add device by IP address" and enter your PC's IP if the phone isn't finding the PC either.
Auto-reconnect: paired devices reconnect automatically after a connection drop. The daemon dials the last known IP with exponential backoff up to 5 minutes. To stop reconnection, unpair the device.
Pair with a device. Two modes depending on whether you provide a device ID.
kcd pair <device-id>If the device has already sent a pair request to kcd (state PairRequestedByPeer), this accepts it. Otherwise, it sends a new pair request — accept on your phone.
kcd pairNo arguments = listen mode. Broadcast is started automatically so the phone can discover the PC. The CLI waits for any incoming pair request and accepts it immediately, printing the verification code:
Listening for pair requests… (Ctrl+C to cancel)
Paired with Pixel 8 Pro (a1b2c3d4...)
Verification code: 3a8f
Broadcast stops when pairing completes or you press Ctrl+C.
Revoke trust and remove a device from the paired list.
kcd unpair <device-id>
This sends a rejection packet to the device and removes it from the local device registry.
Send a ping notification to a device. The phone displays a "Ping!" notification.
kcd ping <device-id>
Fetch the current battery level and charging state of a device.
kcd battery <device-id>
Example output
Battery: 74% (charging)
Battery: 31% (discharging)
For continuous monitoring, use
kcd watch --events=battery.updateinstead.
Push the local clipboard content to a device.
kcd clipboard [device-id]
If device-id is omitted, kcd automatically targets the first connected device.
Clipboard backend detection
- Wayland (
$WAYLAND_DISPLAYset): reads viawl-paste, writes viawl-copy - X11: reads/writes via
xclip
Example — Sway keybinding
bindsym Super+c exec kcd clipboardControl media playback on remote devices (phones) and show their now-playing state.
All action subcommands (play, pause, toggle, next, prev, stop, volume, seek) support auto-device and auto-player — omit --device and --player and they automatically target the first connected phone with cached MPRIS state.
List active media players on remote devices. Refreshes state by sending requestPlayerList to all connected phones.
kcd mpris list [--json]
Flags
| Flag | Description |
|---|---|
--json |
Output as a JSON array |
Examples
# Human-readable
kcd mpris list
# JSON for scripting
kcd mpris list --json | jq -r '.[] | "\(.player): \(.title)"'Show currently playing media on remote devices, with position, volume, and artist info.
kcd mpris status [--device <id>] [--json]
Flags
| Flag | Description |
|---|---|
--device |
Target device ID (omit for auto-detect) |
--json |
Output as a JSON array |
Examples
kcd mpris status
kcd mpris status --json | jq -r '.[].title'Control playback on the phone.
kcd mpris play [--device <id>] [--player <name>]
kcd mpris pause [--device <id>] [--player <name>]
kcd mpris toggle [--device <id>] [--player <name>]
Flags
| Flag | Description |
|---|---|
--device |
Target device ID (omit for auto-detect) |
--player, -p |
Player name (omit for auto-fill from cached state) |
Skip to the next or previous track. After skipping, an automatic Play action is sent to handle phone-side MPRIS implementations that stop after a track change.
kcd mpris next [--device <id>] [--player <name>]
kcd mpris previous [--device <id>] [--player <name>]
kcd mpris prev [--device <id>] [--player <name>] (alias)
Stop playback on the phone.
kcd mpris stop [--device <id>] [--player <name>]
Set the phone's media volume (0–100).
kcd mpris volume [--device <id>] [--player <name>] <0-100>
Example
kcd mpris volume 50Seek to a position or by an offset on the phone.
kcd mpris seek [--device <id>] [--player <name>] <offset>
Offset formats:
+30s— forward 30 seconds-10s— back 10 seconds1m30s— absolute 1 minute 30 seconds45— absolute 45 seconds (bare number)
Examples
kcd mpris seek +30s
kcd mpris seek -10s
kcd mpris seek 1m30sDump raw MPRIS debug state for all local media players as JSON. Shows player names, their running status, and device-to-player mappings. Useful for diagnosing "no player found" issues.
kcd mpris raw
Returns the same data as the mpris_status IPC command.
{
"watcherRunning": true,
"deviceCount": 2,
"players": ["spotify", "firefox"],
"playerMappings": {
"a1b2c3d4e5f6_...": "spotify"
}
}# Get current track title (exit 0 on success)
kcd mpris status --json | jq -r '.[].title'
# Toggle playback and check result
kcd mpris toggle
kcd mpris status --json | jq -r '.[] | "playing=\(.isPlaying)"'
# Next track, wait for state update, print new title
kcd mpris next && sleep 2 && kcd mpris list --json | jq -r '.[] | "\(.title) - \(.artist)"'
# Watch live track changes
kcd watch --events=mpris.update
# Sway keybinding: next track on the phone
# bindsym XF86AudioNext exec kcd mpris nextNote: The
mpris.updateevent fires whenever the phone sends a now-playing state change (track change, play/pause toggle). Subscribe withkcd watch --events=mpris.update.
Send a local file to a device.
kcd share <device-id> <file-path>
The file is transferred over a dedicated TCP side-channel (ports 1739–1764) and saved to the phone's Downloads folder.
Example
kcd share a1b2c3d4_e5f6_7890_abcd_ef1234567890 ~/Documents/report.pdfWatch transfer progress:
kcd watch --events=share.progress,share.completeSend a text reply to a notification that supports it (e.g. a messaging app notification).
kcd reply <device-id> <reply-id> <message>
The reply-id is included in the notification event payload. Capture it with kcd watch --json:
kcd watch --json | jq 'select(.type=="notification") | {id: .payload.id, app: .payload.appName}'Example
kcd reply a1b2... abc-123 "On my way!"Manage phone calls.
Silence the ringtone for an incoming call without answering.
kcd call mute <device-id>
Make the phone play a loud ringtone to help locate it.
kcd findmyphone <device-id>
Lock or unlock the current desktop session.
kcd lock <device-id>
kcd unlock <device-id>
Uses loginctl lock-session / loginctl unlock-session under the hood.
Manage SFTP access to the phone's filesystem.
The phone's SFTP server may expose multiple storage volumes (e.g. "Internal shared storage" and "SD card"). These are reported via multiPaths and pathNames in the KDE Connect protocol.
Ask the device for SFTP credentials (host, port, user, password). The result is emitted as an sftp.mount event.
kcd sftp request <device-id>
Capture the credentials:
kcd watch --json --events=sftp.mount | jq -r 'select(.type=="sftp.mount") | .payload.uri'Show cached SFTP connection details for a paired device, including available storage volumes:
kcd sftp info <device-id>
Example output
Device: a1b2c3d4_e5f6_7890_abcd_ef1234567890 (Pixel 8 Pro)
IP: 192.168.1.50
Port: 8022
User: sftp-user
Password: ********
Volumes:
1. Internal shared storage → /storage/emulated/0
2. SD card → /storage/ABCD-1234
If the phone returned an error (e.g. storage permission not granted), the errorMessage field is shown instead.
List available storage volumes without the full info output:
kcd sftp volumes <device-id>
Example output
Internal shared storage → /storage/emulated/0
SD card → /storage/ABCD-1234
The phone must have an active SFTP session (run
kcd sftp requestfirst if the volumes list is empty).
Request credentials and immediately mount the phone's filesystem using sshfs.
kcd sftp mount <device-id>
The mount point is printed to stdout. Unmount with fusermount -u <mountpoint>.
Cleanly unmount a previously mounted phone filesystem.
kcd sftp unmount <device-id>
Calls fusermount3 (or fusermount on older systems) and removes the
temporary mount point directory. Returns an error if the device was never
mounted in this daemon session.
Request fresh SFTP credentials and list available storage volumes or mount a specific one immediately.
kcd sftp browse <device-id> [volume-index|volume-name|volume-path]
Without a volume argument — requests fresh credentials and lists volumes:
$ kcd sftp browse a1b2c3d4
Requesting SFTP credentials from phone (waiting up to 20s)…
Available volumes:
0. Internal shared storage /storage/emulated/0
1. SD card /storage/ABCD-1234
Mount a volume: kcd sftp browse <device-id> <index|name|path>
With a volume argument — requests fresh credentials and mounts the specified volume via sshfs, opening it in the default file manager:
$ kcd sftp browse a1b2c3d4 "SD card"
Requesting SFTP credentials from phone (waiting up to 20s)…
Mounted at: /home/user/Downloads/kcd/mnt/kcd-sftp-a1b2c3d4
The volume argument is resolved in this order:
- Index (0-based) —
0,1, etc. - Name (case-insensitive) —
"SD card","internal shared storage" - Path (exact then case-insensitive) —
/storage/ABCD-1234
This is the recommended way to mount a specific phone volume without having
to request + mount separately. It always fetches fresh credentials,
so the credentials are guaranteed valid.
Execute and manage remote commands configured on the phone's KDE Connect app, or expose local shell commands to the phone via the [commands] config table.
Request the list of commands available on the remote device.
kcd run list <device-id>
Results arrive as a runcommand.list event; watch for them:
kcd watch --json | jq 'select(.type=="runcommand.list")'Execute one of the local commands registered in [commands] config, triggered from the phone, or send a command execution request to the phone.
kcd run exec <device-id> <command-key>
Example config (kcd.toml)
[commands]
uptime = "uptime"
lock = "loginctl lock-session"
suspend = "systemctl suspend"kcd run exec a1b2... uptimeSend and receive SMS via a connected phone.
Send an SMS message.
kcd sms send <device-id> <phone-number> <message>
Example
kcd sms send a1b2... +1555000111 "Heading home in 10"Request a list of SMS conversation threads. Results arrive as sms.incoming events.
kcd sms conversations <device-id>
Request messages from a specific conversation thread.
kcd sms conversation <device-id> <thread-id>
Request an MMS attachment file from a device. The file is saved locally and an
sms.attachment event is emitted with the path.
kcd sms attachment <device-id> <part-id> <unique-identifier>
Control the remote device's audio volume (requires remotesystemvolume plugin).
List audio sinks on a remote device and their current volume/mute state.
kcd volume list <device-id>
Example output
media 75%
alarm 100%
Set a sink's volume (0–100) on a remote device.
kcd volume set <device-id> <sink-name> <0-100>
Examples
kcd volume set a1b2... media 50
kcd volume set a1b2... alarm 80Mute or unmute a sink on a remote device.
kcd volume mute <device-id> <sink-name> <true|false>
Examples
kcd volume mute a1b2... media true # Mute
kcd volume mute a1b2... media false # UnmuteSink names and current volume can be discovered with
kcd volume list. Volume changes made on the phone are also published asvolume.updateevents.
Monitor real-time events from the daemon as an NDJSON stream. This is the primary way to observe what is happening across all devices.
For the full list of event types and their payload fields, see README.md § Events.
kcd watch [--events <type,...>] [--json]
Flags
| Flag | Description |
|---|---|
--events, -e |
Comma-separated list of event types to filter (default: all) |
--json |
Output raw NDJSON instead of human-readable text |
kcd watch reconnects automatically if the daemon restarts, with exponential backoff up to 30 seconds.
| Event type | Description |
|---|---|
device.added |
A new device was seen for the first time |
device.removed |
A device was unpaired and removed |
device.connected |
A device established a TCP connection |
device.disconnected |
A device's connection dropped |
pair.requested |
The phone sent a pairing request |
pair.accepted |
Pairing was accepted on both sides |
pair.rejected |
Pairing was denied or cancelled |
battery.update |
New battery reading: {charge, charging} |
notification |
Notification from the phone: {appName, title, text, id, ...} |
notification.canceled |
The phone dismissed a notification: {id} |
share.progress |
File transfer progress: {file, current, total} |
share.complete |
File transfer finished: {file, success, error?} |
share.text |
Plain text received: {text} |
share.url |
URL received: {url} |
ping.received |
A ping arrived |
telephony.ringing |
Incoming call: {contactName, phoneNumber} |
telephony.missed |
Missed call: {contactName, phoneNumber} |
telephony.canceled |
Call ended |
connectivity.update |
Signal strength: {signal, networkType} |
volume.update |
Device volume changed: {name, volume, muted} |
mpris.update |
Now playing: {player, title, artist, album, isPlaying, pos, length, volume} |
sftp.mount |
SFTP credentials: {uri, ip, port, user, password, path, multiPaths, pathNames, errorMessage} |
battery.threshold |
Battery low/full alert: {charge, charging, event} |
telephony.talking |
Call in progress: {contactName, phoneNumber} |
sms.incoming |
SMS/MMS received: {body, sender, date, thread_id, read} |
sms.attachment |
MMS attachment downloaded: {filename, path, thread_id} |
ring.received |
Phone wants this PC to ring |
Watch everything, human-readable
kcd watch[a1b2...] battery: 62% (charging: false)
[a1b2...] notification: WhatsApp - Alice: "Hey, are you free?"
[a1b2...] telephony.ringing — Bob (+15550001234)
Filter to battery and calls only
kcd watch --events=battery.update,telephony.ringingRaw NDJSON for scripting
kcd watch --json{"type":"battery.update","timestamp":"2025-04-21T10:22:01Z","deviceId":"a1b2...","payload":{"charge":62,"charging":false}}
{"type":"notification","timestamp":"2025-04-21T10:22:45Z","deviceId":"a1b2...","payload":{"appName":"WhatsApp","title":"Alice","text":"Hey, are you free?","id":"notif-abc"}}Waybar phone battery integration
kcd watch --json | jq -r '
select(.type=="battery.update") |
"\(.payload.charge)%" + (if .payload.charging then " " else "" end)
'Desktop notification on incoming call
kcd watch --json --events=telephony.ringing | \
jq -r '.payload | "Call from \(.contactName // .phoneNumber)"' | \
xargs -I{} notify-send "📞 Incoming Call" "{}"Auto-mute calls when screen is locked
kcd watch --json --events=telephony.ringing | while read -r ev; do
id=$(echo "$ev" | jq -r '.deviceId')
if loginctl show-session self -p LockedHint | grep -q yes; then
kcd call mute "$id"
fi
doneGet the first connected device ID
kcd devices --json | jq -r '[.[] | select(.Connected)] | .[0].ID'Send a file from a Nautilus script
Install packaging/nautilus-kcd.py for right-click → "Send to phone" in GNOME Files.
Debug connection issues
kcd daemon --log-level=debug 2>&1 | grep -E "discovery|transport|pair"Broadcast is off by default (only active during
kcd pairlisten mode). The daemon sits at 0.0% idle CPU — no config change needed.