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 meshexecRun MeshExec as a docker container by mounting your configuration directory file to /root/.config/meshexec/. For example:
docker run -d \
--name meshexec \
--device /dev/ttyUSB0:/dev/ttyUSB0 \ # Pass through the serial device (update this path as needed; should match path in config)
-v /home/aclarke/.config/meshexec:/root/.config/meshexec/:ro \
darkalex17/meshexec:latestYou can also clone this repo and run just build-docker to build a docker image locally and run it using the above command.
Please note that you will need to create and populate your configuration file(s) first before starting the container. Otherwise, the container will fail to start.
Be sure to also pass through the correct serial device path (e.g. /dev/ttyUSB0) that matches the device field in your configuration file.
To 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
default: "8080" # All default values must be strings; raw YAML types like boolean and integers are not supported for default values
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


