MeshExec lets you execute commands on remote serially-connected Meshtastic nodes by listening for messages in a
private channel. Define command aliases with arguments and flags in a YAML config, send a message like !myip over the
mesh, and get the output back — no internet required!
- MeshExec connects to a Meshtastic device via serial port
- It listens for messages prefixed with
!on a configured private channel - When a matching command alias is received, it executes the corresponding shell command
- The output is chunked to fit within Meshtastic's message size limits and sent back over the mesh
This makes it ideal for managing remote devices in off-grid, decentralized, or IoT deployments where traditional network access isn't available.
- A Meshtastic device connected via serial (USB)
- A private channel configured on the device
- Rust 1.89.0+ (for building from source)
If you have Cargo installed, then you can install MeshExec from Crates.io:
cargo install meshexec
# If you encounter issues installing, try installing with '--locked'
cargo install --locked meshexecTo install MeshExec from Homebrew, install the MeshExec tap. Then you'll be able to install MeshExec:
brew tap Dark-Alex-17/meshexec
brew install meshexec
# If you need to be more specific, use the following:
brew install Dark-Alex-17/meshexec/meshexecTo upgrade to a newer version of MeshExec:
brew upgrade meshexecBinaries are available on the releases page.
- Download the latest binary for your OS and architecture.
cdto the directory where you downloaded the binary.- Extract the binary with
tar -C /usr/local/bin -xzf meshexec-<arch>.tar.gz(Note: This may requiresudo) - Now you can run
meshexec!
MeshExec has three subcommands:
Starts the runner server that listens for commands on the mesh network:
# Config file 'config.yml' is in current directory
meshexec serve
# Config file 'config.yml' is in another directory
meshexec --config-file /opt/meshexec/config.yml serveTails the MeshExec log file with optional colored output:
meshexec tail-logs
# Disable colored output
meshexec tail-logs --no-colorPrints the default configuration file path for your system:
meshexec config-pathThis is useful for finding where to place your configuration file. The output varies by operating system:
- Linux:
~/.config/meshexec/config.yaml - macOS:
~/Library/Application Support/meshexec/config.yaml - Windows:
C:\Users\<User>\AppData\Roaming\meshexec\config.yaml
| Flag | Short | Env Var | Description |
|---|---|---|---|
--config-file <PATH> |
-c |
MESHEXEC_CONFIG_FILE |
Specify the config file (if not set, searches current directory then system config directory; see Configuration File Location) |
--log-level <LEVEL> |
-l |
MESHEXEC_LOG_LEVEL |
Set the logging level: off, error, warn, info (default), debug, trace |
Once MeshExec is running, send messages prefixed with ! on the configured private channel from any node on the mesh:
!help # List all available commands
!myip # Run the 'myip' command
!network check-port 8080 # Run a subcommand with an argument
!loki --help # Show help for a specific command
MeshExec is configured via a YAML file. You can specify an explicit path with --config-file, or let MeshExec
automatically find for a configuration file.
MeshExec searches for a configuration file in the following order:
- Explicit path: If
--config-fileorMESHEXEC_CONFIG_FILEis set, that path is used directly - Current directory:
./config.yamlor./config.yml - System config directory: The standard configuration directory for your operating system
To find the system config directory for your platform, run:
meshexec config-pathIf no configuration file is found in any of these locations, MeshExec will display an error listing all searched paths.
device: /dev/ttyUSB0
channel: 1
baud: null
shell: bash
shell_args:
- -lc
max_text_bytes: 200
chunk_delay: 10000
max_content_bytes: 180
commands:
- import: network_commands.yml
- name: loki
help: Ask Loki something
args:
- name: question
help: Your prompt for Loki
greedy: true
command: loki "${question}"
- name: list-disk-space
help: List disk space for all mounted filesystems
args:
- name: servarr
help: The servarr to hit
flags:
- long: --servarr-name
short: -s
arg: servarr_name
help: The name of the servarr instance
command: |
# Can define scripts inline
declare -a flags=()
if [[ -n $servarr_name ]]; then
flags+=("--servarr-name $servarr_name")
fi
managarr $servarr "${flags[@]}"See the examples/ directory for a full configuration example (i.e. with subcommands).
| Field | Type | Required | Description |
|---|---|---|---|
device |
string |
Yes | Serial device path (e.g. /dev/ttyUSB0, /dev/tty.usbserial-0001) |
channel |
integer |
Yes | Meshtastic channel number to listen on (must be a private channel) |
baud |
integer |
No | Baud rate for the serial connection (uses the Meshtastic default if null) |
shell |
string |
Yes | Shell to execute commands with (e.g. bash, sh, zsh) |
shell_args |
list[string] |
No | Arguments to pass to the shell (e.g. ["-lc"] for a login shell with command) |
max_text_bytes |
integer |
Yes | Maximum bytes per Meshtastic text message (device-dependent, typically ~200) |
chunk_delay |
integer |
Yes | Delay in milliseconds between sending chunks (prevents flooding the mesh) |
max_content_bytes |
integer |
Yes | Maximum content bytes per chunk before footer (should be less than max_text_bytes to leave room for [1/N] footers) |
commands |
list |
Yes | List of command definitions and/or imports |
Commands can be either leaf commands (execute a shell command) or group commands (contain subcommands). They can also be imported from external YAML files, enabling more complex configuration structures.
- name: myip
help: Show the current system's public IP address
command: curl -s checkip.amazonaws.com| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | The alias name (used after ! prefix, e.g. !myip) |
help |
string |
No | Help text shown when the user sends !<command> --help |
command |
string |
Yes (for leaf) | Shell command to execute. Use ${var_name} to interpolate arg/flag values |
args |
list[Arg] |
No | Positional arguments |
flags |
list[Flag] |
No | Named flags |
Group commands organize subcommands under a namespace:
# network_commands.yml
- name: network
help: Network commands
commands:
- name: myip
command: curl -s checkip.amazonaws.com
- name: check-port
args:
- name: port
help: The port number to check
command: 'sudo lsof -i :${port}'| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | The group name |
help |
string |
No | Help text for the group |
commands |
list |
Yes (for group) | Nested subcommands and/or imports (recursive) |
A command cannot have both command and commands — it must be one or the other. Group commands cannot have
args or flags.
Commands can be split across multiple YAML files using imports:
commands:
- import: network_commands.yml
- import: monitoring_commands.yml
- name: inline-command
command: echo "I'm defined inline"The imported file can contain either a single command object or a list of commands. Circular imports are detected and will produce an error.
Imports can also be used inside group commands, enabling deeply nested command hierarchies organized across multiple files:
---
# config.yml
commands:
- import: network_commands.yml
---
# network_commands.yml
name: network
help: Network commands
commands:
- import: docker_commands.yml # Imports can be nested inside groups
- name: myip
command: curl -s checkip.amazonaws.com
---
# docker_commands.yml
name: docker
help: Docker commands
commands:
- name: hello
command: docker run hello-worldThis creates a command hierarchy where you can run:
!network myip— Show public IP!network docker hello— Run the Docker hello-world container
Import paths are always relative to the file containing the import directive.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Argument name (used as the environment variable name; hyphens become underscores) |
help |
string |
Yes | Help text shown in --help output |
default |
string |
No | Default value if not provided (if omitted, the argument is required) |
greedy |
bool |
No | If true, consumes all remaining tokens. Must be the last arg. Default: false |
| Field | Type | Required | Description |
|---|---|---|---|
long |
string |
Yes | Long flag name (must start with --, e.g. --verbose) |
short |
string |
No | Short flag alias (must be - followed by a single character, e.g. -v) |
help |
string |
No | Help text shown in --help output |
arg |
string |
No | If present, the flag takes a value (the string is the env var name). If absent, the flag is boolean |
required |
bool |
No | If true, the flag must be provided. Default: false |
default |
string |
No | Default value when the flag is not provided |
greedy |
bool |
No | If true, consumes all remaining tokens as the value. Requires arg to be set. Must be the last flag. Default: false |
Only one arg or flag in a command can be greedy, and it must be the last in its respective list. A greedy arg/flag consumes all remaining whitespace-separated tokens as a single value. This is useful for free-text inputs:
- name: ask
help: Ask a question
args:
- name: question
help: Your question
greedy: true
command: echo "${question}"Sending !ask what is the weather today would set question to "what is the weather today".
| Variable | Description | Equivalent Flag |
|---|---|---|
MESHEXEC_CONFIG_FILE |
Path to the config file | --config-file |
MESHEXEC_LOG_LEVEL |
Logging level (off, error, warn, info, debug, trace) |
--log-level |
See the CONTRIBUTING.md for details on how to contribute to this project.
- meshtastic - Meshtastic protocol library for Rust
- clap - Command line argument parsing
- tokio - Async runtime
- serde - Serialization/deserialization framework
- log4rs - Logging framework


