diff --git a/lib/pb-protocol/pb-protocol.c b/lib/pb-protocol/pb-protocol.c
index b4138bbf..a3c4a031 100644
--- a/lib/pb-protocol/pb-protocol.c
+++ b/lib/pb-protocol/pb-protocol.c
@@ -346,6 +346,41 @@ int pb_protocol_url_len(const char *url)
 	return 4 + optional_strlen(url);
 }
 
+int pb_protocol_command_len(const struct command *cmd)
+{
+	unsigned int i, len = 0;
+
+	len += 4 + optional_strlen(cmd->platform);
+	len += 4 + optional_strlen(cmd->name);
+	len += 4 + optional_strlen(cmd->cmd);
+	len += 4 + optional_strlen(cmd->args_fmt);
+	len += 4; /* n_args */
+
+	for (i = 0; i < cmd->n_args; i++) {
+		len += 4 + optional_strlen(cmd->args[i].name);
+		len += 4; /* type */
+		switch(cmd->args[i].type) {
+		case ARG_STR:
+			len += 4 + optional_strlen(cmd->args[i].arg_str);
+			break;
+		case ARG_I64:
+			len += sizeof(cmd->args[i].arg_i64);
+			break;
+		case ARG_F64:
+			len += sizeof(cmd->args[i].arg_f64);
+			break;
+		default:
+			pb_log("Argument %s has unknown field type %d\n",
+					cmd->args[i].name,
+					cmd->args[i].type);
+			break;
+		}
+	}
+
+	len += 4 + optional_strlen(cmd->help);
+
+	return len;
+}
 
 int pb_protocol_plugin_option_len(const struct plugin_option *opt)
 {
@@ -363,6 +398,10 @@ int pb_protocol_plugin_option_len(const struct plugin_option *opt)
 	for (i = 0; i < opt->n_executables; i++)
 		len += 4 + optional_strlen(opt->executables[i]);
 
+	len += 4; /* command options */
+	for (i = 0; i < opt->n_commands; i++)
+		len += pb_protocol_command_len(&opt->commands[i]);
+
 	return len;
 }
 
@@ -682,6 +721,51 @@ int pb_protocol_serialise_url(const char *url, char *buf, int buf_len)
 	return 0;
 }
 
+int pb_protocol_serialise_command(char *buf,
+		const struct command *config)
+{
+	char *pos = buf;
+	unsigned int i;
+
+	pos += pb_protocol_serialise_string(pos, config->platform);
+	pos += pb_protocol_serialise_string(pos, config->name);
+	pos += pb_protocol_serialise_string(pos, config->cmd);
+	pos += pb_protocol_serialise_string(pos, config->args_fmt);
+
+	*(uint32_t *)pos = __cpu_to_be32(config->n_args);
+	pos += 4;
+
+	for (i = 0; i < config->n_args; i++) {
+		pos += pb_protocol_serialise_string(pos, config->args[i].name);
+		*(enum cmd_arg_type *)pos = config->args[i].type;
+		pos += sizeof(enum cmd_arg_type);
+
+		switch(config->args[i].type) {
+		case ARG_STR:
+			pos += pb_protocol_serialise_string(pos,
+					config->args[i].arg_str);
+			break;
+		case ARG_I64:
+			*(int64_t *)pos = __cpu_to_be64(config->args[i].arg_i64);
+			pos += sizeof(int64_t);
+			break;
+		case ARG_F64:
+			memcpy(pos, &config->args[i].arg_f64, sizeof(double));
+			pos += sizeof(double);
+			break;
+		default:
+			pb_log("Argument %s with unknown field type %d skipped\n",
+					config->args[i].name,
+					config->args[i].type);
+			break;
+		}
+	}
+
+	pos += pb_protocol_serialise_string(pos, config->help);
+
+	return pos - buf;
+}
+
 int pb_protocol_serialise_plugin_option(const struct plugin_option *opt,
 		char *buf, int buf_len)
 {
@@ -702,6 +786,13 @@ int pb_protocol_serialise_plugin_option(const struct plugin_option *opt,
 	for (i = 0; i < opt->n_executables; i++)
 		pos += pb_protocol_serialise_string(pos, opt->executables[i]);
 
+	*(uint32_t *)pos = __cpu_to_be32(opt->n_commands);
+	pos += 4;
+
+	for (i = 0; i < opt->n_commands; i++)
+		pos += pb_protocol_serialise_command(pos,
+				&opt->commands[i]);
+
 	assert(pos <= buf + buf_len);
 	(void)buf_len;
 
@@ -1318,6 +1409,71 @@ out:
 	return rc;
 }
 
+int pb_protocol_deserialise_command(void *ctx, const char **pos,
+		unsigned int *len, struct command *cmd)
+{
+	unsigned int i;
+	char *str;
+	int rc = -1;
+
+	if (read_string(ctx, pos, len, &str))
+		goto out;
+	cmd->platform = str;
+
+	if (read_string(ctx, pos, len, &str))
+		goto out;
+	cmd->name = str;
+
+	if (read_string(ctx, pos, len, &str))
+		goto out;
+	cmd->cmd = str;
+
+	if (read_string(ctx, pos, len, &str))
+		goto out;
+	cmd->args_fmt = str;
+
+	if (read_u32(pos, len, &cmd->n_args))
+		goto out;
+
+	cmd->args = talloc_zero_array(ctx, struct argument, cmd->n_args);
+	for (i = 0; i < cmd->n_args; i++) {
+		if (read_string(ctx, pos, len, &str))
+			goto out;
+		cmd->args[i].name = str;
+
+		cmd->args[i].type = *(enum cmd_arg_type *)*pos;
+		*pos += sizeof(enum cmd_arg_type);
+
+		switch (cmd->args[i].type) {
+		case ARG_STR:
+			if (read_string(ctx, pos, len, &str))
+				goto out;
+			cmd->args[i].arg_str = str;
+			break;
+		case ARG_I64:
+			cmd->args[i].arg_i64 = __be64_to_cpu(*(int64_t *)(*pos));
+			*pos += sizeof(int64_t);
+			break;
+		case ARG_F64:
+			memcpy(&cmd->args[i].arg_f64, *pos, sizeof(double));
+			*pos += sizeof(double);
+			break;
+		default:
+			pb_log("Unknown field type %d for argument %s\n",
+					cmd->args[i].type,
+					cmd->args[i].name);
+		}
+	}
+
+	if (read_string(ctx, pos, len, &str))
+		goto out;
+	cmd->help = str;
+
+	rc = 0;
+out:
+	return rc;
+}
+
 int pb_protocol_deserialise_plugin_option(struct plugin_option *opt,
 		const struct pb_protocol_message *message)
 {
@@ -1371,6 +1527,18 @@ int pb_protocol_deserialise_plugin_option(struct plugin_option *opt,
 		opt->executables[i] = talloc_strdup(opt, str);
 	}
 
+	if (read_u32(&pos, &len, &tmp))
+		goto out;
+	opt->n_commands = tmp;
+
+	opt->commands = talloc_zero_array(opt, struct command, opt->n_commands);
+	if (!opt->commands)
+		goto out;
+
+	for (i = 0; i < opt->n_commands; i++)
+		pb_protocol_deserialise_command(opt, &pos, &len,
+				&opt->commands[i]);
+
 	rc = 0;
 out:
 	return rc;
diff --git a/lib/pb-protocol/pb-protocol.h b/lib/pb-protocol/pb-protocol.h
index 1d6c0485..38b241ce 100644
--- a/lib/pb-protocol/pb-protocol.h
+++ b/lib/pb-protocol/pb-protocol.h
@@ -63,6 +63,7 @@ int pb_protocol_boot_status_len(const struct status *status);
 int pb_protocol_system_info_len(const struct system_info *sysinfo);
 int pb_protocol_config_len(const struct config *config);
 int pb_protocol_url_len(const char *url);
+int pb_protocol_command_len(const struct command *command);
 int pb_protocol_plugin_option_len(const struct plugin_option *opt);
 int pb_protocol_temp_autoboot_len(const struct autoboot_option *opt);
 int pb_protocol_authenticate_len(struct auth_message *msg);
@@ -88,6 +89,8 @@ int pb_protocol_serialise_system_info(const struct system_info *sysinfo,
 int pb_protocol_serialise_config(const struct config *config,
 		char *buf, int buf_len);
 int pb_protocol_serialise_url(const char *url, char *buf, int buf_len);
+int pb_protocol_serialise_command(char *buf,
+		const struct command *command);
 int pb_protocol_serialise_plugin_option(const struct plugin_option *opt,
 		char *buf, int buf_len);
 int pb_protocol_serialise_temp_autoboot(const struct autoboot_option *opt,
@@ -120,6 +123,9 @@ int pb_protocol_deserialise_system_info(struct system_info *sysinfo,
 int pb_protocol_deserialise_config(struct config *config,
 		const struct pb_protocol_message *message);
 
+int pb_protocol_deserialise_command(void *ctx, const char **pos,
+		unsigned int *len, struct command *command);
+
 int pb_protocol_deserialise_plugin_option(struct plugin_option *opt,
 		const struct pb_protocol_message *message);
 
diff --git a/test/lib/Makefile.am b/test/lib/Makefile.am
index 65991a55..1f49bbce 100644
--- a/test/lib/Makefile.am
+++ b/test/lib/Makefile.am
@@ -24,7 +24,8 @@ lib_TESTS = \
 	test/lib/test-process-both \
 	test/lib/test-process-stdout-eintr \
 	test/lib/test-fold \
-	test/lib/test-efivar
+	test/lib/test-efivar \
+	test/lib/test-protocol
 
 if WITH_OPENSSL
 lib_TESTS += \
diff --git a/test/lib/test-protocol.c b/test/lib/test-protocol.c
new file mode 100644
index 00000000..18cb7f5d
--- /dev/null
+++ b/test/lib/test-protocol.c
@@ -0,0 +1,158 @@
+
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#include <types/types.h>
+#include <talloc/talloc.h>
+#include <pb-protocol/pb-protocol.h>
+
+static int test_command_protocol(void *ctx)
+{
+	unsigned int len, write_len, read_len;
+	struct command *write, *read;
+	const char *read_buf;
+	char *buf, *pos;
+
+	write = talloc_zero(ctx, struct command);
+	if (!write)
+		return -1;
+
+	write->platform = talloc_asprintf(ctx, "platform");
+	write->name = talloc_asprintf(ctx, "test config");
+	write->cmd = talloc_asprintf(ctx, "command");
+	write->args_fmt = talloc_asprintf(ctx, "{} {} {}");
+	write->n_args = 3;
+	write->args = talloc_zero_array(write, struct argument, write->n_args);
+
+	write->args[0].name = talloc_asprintf(write, "arg 0");
+	write->args[0].type = ARG_STR;
+	write->args[0].arg_str = talloc_asprintf(write, "string arg");
+
+	write->args[1].name = talloc_asprintf(write, "arg 1");
+	write->args[1].type = ARG_I64;
+	write->args[1].arg_i64 = 5;
+
+	write->args[2].name = talloc_asprintf(write, "arg 2");
+	write->args[2].type = ARG_F64;
+	write->args[2].arg_f64 = 4.4;
+
+	write->help = talloc_asprintf(write, "good luck");
+
+	len = pb_protocol_command_len(write);
+	fprintf(stderr, "write config is %d bytes\n", len);
+
+	buf = talloc_array(ctx, char, len);
+	pos = buf;
+
+	write_len = pb_protocol_serialise_command(pos, write);
+	if (write_len != len) {
+		fprintf(stderr, "Failed to serialise machine config\n");
+		fprintf(stderr, "Serialised length does not match expected (%lu vs %d)\n",
+				pos - buf, write_len);
+		return -1;
+	}
+
+	read_buf = talloc_memdup(ctx, buf, write_len);
+	read_len = write_len;
+	read = talloc_zero(ctx, struct command);
+	if (pb_protocol_deserialise_command(ctx, &read_buf, &read_len, read)) {
+		fprintf(stderr, "Failed to deserialise machine config\n");
+		return -1;
+	}
+
+	if (strcmp(write->platform, read->platform)) {
+		fprintf(stderr, "platform field does not match: %s vs %s\n",
+				write->platform, read->platform);
+		return -1;
+	}
+	if (strcmp(write->name, read->name)) {
+		fprintf(stderr, "name field does not match: %s vs %s\n",
+				write->name, read->name);
+		return -1;
+	}
+	if (strcmp(write->cmd, read->cmd)) {
+		fprintf(stderr, "cmd field does not match: %s vs %s\n",
+				write->cmd, read->cmd);
+		return -1;
+	}
+	if (strcmp(write->args_fmt, read->args_fmt)) {
+		fprintf(stderr, "args_fmt field does not match: %s vs %s\n",
+				write->args_fmt, read->args_fmt);
+		return -1;
+	}
+
+	if (write->n_args != read->n_args) {
+		fprintf(stderr, "n_args mismatch: %u vs %u\n", write->n_args,
+				read->n_args);
+		return -1;
+	}
+
+	if (strcmp(write->args[0].name, read->args[0].name)) {
+		fprintf(stderr, "arg 0 name does not match: %s vs %s\n",
+				write->args[0].name, read->args[0].name);
+		return -1;
+	}
+	if (write->args[0].type != read->args[0].type) {
+		fprintf(stderr, "arg 0 field type does not match: %d vs %d\n",
+				write->args[0].type, read->args[0].type);
+		return -1;
+	}
+	if (strcmp(write->args[0].arg_str, read->args[0].arg_str)) {
+		fprintf(stderr, "arg 0 arg does not match: %s vs %s\n",
+				write->args[0].arg_str, read->args[0].arg_str);
+		return -1;
+	}
+
+	if (strcmp(write->args[1].name, read->args[1].name)) {
+		fprintf(stderr, "arg 1 name does not match: %s vs %s\n",
+				write->args[1].name, read->args[1].name);
+		return -1;
+	}
+	if (write->args[1].type != read->args[1].type) {
+		fprintf(stderr, "arg 1 field type does not match: %d vs %d\n",
+				write->args[1].type, read->args[1].type);
+		return -1;
+	}
+	if (write->args[1].arg_i64 != read->args[1].arg_i64) {
+		fprintf(stderr, "arg 1 field arg does not match: %ld vs %ld\n",
+				write->args[1].arg_i64, read->args[1].arg_i64);
+		return -1;
+	}
+
+	if (strcmp(write->args[2].name, read->args[2].name)) {
+		fprintf(stderr, "arg 2 name does not match: %s vs %s\n",
+				write->args[2].name, read->args[2].name);
+		return -1;
+	}
+	if (write->args[2].type != read->args[2].type) {
+		fprintf(stderr, "arg 2 field type does not match: %d vs %d\n",
+				write->args[2].type, read->args[2].type);
+		return -1;
+	}
+	if (write->args[2].arg_f64 != read->args[2].arg_f64) {
+		fprintf(stderr, "arg 2 field arg does not match: %f vs %f\n",
+				write->args[2].arg_f64, read->args[2].arg_f64);
+		return -1;
+	}
+
+	return 0;
+}
+
+int main(void)
+{
+	void *ctx;
+	int rc = 0;
+
+	ctx = talloc_new(NULL);
+
+	rc = test_command_protocol(ctx);
+	if (rc)
+		printf("FAIL: test_command\n");
+
+	talloc_free(ctx);
+
+	return rc ? EXIT_FAILURE : EXIT_SUCCESS;
+}
