diff --git a/runner/executor.c b/runner/executor.c
index a23cd58c0781d072807060b1027cd3c9525aa99a..ac73e1ddec07b8ceb34852b4f2d95b1180d7bffd 100644
--- a/runner/executor.c
+++ b/runner/executor.c
@@ -1564,10 +1564,10 @@ execute_test_process(int outfd, int errfd, int socketfd,
 		}
 	}
 
-	if (settings->hook_str) {
+	for (size_t i = 0; i < igt_vec_length(&settings->hook_strs); i++) {
 		arg = strdup("--hook");
 		igt_vec_push(&arg_vec, &arg);
-		arg = strdup(settings->hook_str);
+		arg = strdup(*((char **)igt_vec_elem(&settings->hook_strs, i)));
 		igt_vec_push(&arg_vec, &arg);
 	}
 
diff --git a/runner/runner_tests.c b/runner/runner_tests.c
index 7470cc24052ff5884a61079fbe4e7db4eec4295a..b806d45adbd63bef07cd8d954dc093d2612a3224 100644
--- a/runner/runner_tests.c
+++ b/runner/runner_tests.c
@@ -202,7 +202,14 @@ static void assert_settings_equal(struct settings *one, struct settings *two)
 	igt_assert_eq(one->piglit_style_dmesg, two->piglit_style_dmesg);
 	igt_assert_eq(one->dmesg_warn_level, two->dmesg_warn_level);
 	igt_assert_eq(one->prune_mode, two->prune_mode);
-	igt_assert_eqstr(one->hook_str, two->hook_str);
+
+	igt_assert_eq(igt_vec_length(&one->hook_strs), igt_vec_length(&two->hook_strs));
+	for (size_t i = 0; i < igt_vec_length(&one->hook_strs); i++) {
+		char **hook_str_one = igt_vec_elem(&one->hook_strs, i);
+		char **hook_str_two = igt_vec_elem(&two->hook_strs, i);
+
+		igt_assert_eqstr(*hook_str_one, *hook_str_two);
+	}
 }
 
 static void assert_job_list_equal(struct job_list *one, struct job_list *two)
@@ -294,6 +301,7 @@ igt_main
 		igt_assert_eq(settings->include_regexes.size, 0);
 		igt_assert_eq(settings->exclude_regexes.size, 0);
 		igt_assert(igt_list_empty(&settings->env_vars));
+		igt_assert(!igt_vec_length(&settings->hook_strs));
 		igt_assert(!settings->sync);
 		igt_assert_eq(settings->log_level, LOG_LEVEL_NORMAL);
 		igt_assert(!settings->overwrite);
@@ -303,7 +311,6 @@ igt_main
 		igt_assert_eq(settings->overall_timeout, 0);
 		igt_assert(!settings->use_watchdog);
 		igt_assert_eq(settings->prune_mode, 0);
-		igt_assert(!settings->hook_str);
 		igt_assert(strstr(settings->test_root, "test-root-dir") != NULL);
 		igt_assert(strstr(settings->results_path, "path-to-results") != NULL);
 
@@ -467,6 +474,7 @@ igt_main
 				       "--coverage-per-test",
 				       "--collect-script", "/usr/bin/true",
 				       "--hook", "echo hello",
+				       "--hook", "echo world",
 				       "--prune-mode=keep-subtests",
 				       "test-root-dir",
 				       "path-to-results",
@@ -506,6 +514,10 @@ igt_main
 		igt_assert_eqstr(env_var->key, "ENVS_WITH_JUST_KEYS");
 		igt_assert_eqstr(env_var->value, "SHOULD_WORK");
 
+		igt_assert_eq(igt_vec_length(&settings->hook_strs), 2);
+		igt_assert_eqstr(*((char **)igt_vec_elem(&settings->hook_strs, 0)), "echo hello");
+		igt_assert_eqstr(*((char **)igt_vec_elem(&settings->hook_strs, 1)), "echo world");
+
 		igt_assert(settings->sync);
 		igt_assert_eq(settings->log_level, LOG_LEVEL_VERBOSE);
 		igt_assert(settings->overwrite);
@@ -514,7 +526,6 @@ igt_main
 		igt_assert_eq(settings->per_test_timeout, 72);
 		igt_assert_eq(settings->overall_timeout, 360);
 		igt_assert(settings->use_watchdog);
-		igt_assert_eqstr(settings->hook_str, "echo hello");
 		igt_assert_eq(settings->prune_mode, PRUNE_KEEP_SUBTESTS);
 		igt_assert(strstr(settings->test_root, "test-root-dir") != NULL);
 		igt_assert(strstr(settings->results_path, "path-to-results") != NULL);
@@ -966,6 +977,8 @@ igt_main
 					       "--piglit-style-dmesg",
 					       "--prune-mode=keep-all",
 					       "--hook", "echo hello",
+					       "--hook", "echo hello\necho newline",
+					       "--hook", "echo hello\necho newline\\still the second line",
 					       testdatadir,
 					       dirname,
 			};
diff --git a/runner/settings.c b/runner/settings.c
index 6fd742cc826df0c18b9e12bcada83c75c7b97d50..94b3b9fe641f5dd5b7389ac89a3a1e2dc84c6ed9 100644
--- a/runner/settings.c
+++ b/runner/settings.c
@@ -1,4 +1,5 @@
 #include "igt_hook.h"
+#include "igt_vec.h"
 #include "settings.h"
 #include "version.h"
 
@@ -84,6 +85,7 @@ static struct {
 
 static const char settings_filename[] = "metadata.txt";
 static const char env_filename[] = "environment.txt";
+static const char hooks_filename[] = "hooks.txt";
 
 static bool set_log_level(struct settings* settings, const char *level)
 {
@@ -518,6 +520,13 @@ static void free_env_vars(struct igt_list_head *env_vars) {
 	}
 }
 
+static void free_hook_strs(struct igt_vec *hook_strs)
+{
+	for (size_t i = 0; i < igt_vec_length(hook_strs); i++)
+		free(*((char **)igt_vec_elem(hook_strs, i)));
+	igt_vec_fini(hook_strs);
+}
+
 static bool file_exists_at(int dirfd, const char *filename)
 {
 	return faccessat(dirfd, filename, F_OK, 0) == 0;
@@ -620,6 +629,7 @@ void init_settings(struct settings *settings)
 {
 	memset(settings, 0, sizeof(*settings));
 	IGT_INIT_LIST_HEAD(&settings->env_vars);
+	igt_vec_init(&settings->hook_strs, sizeof(char *));
 }
 
 void clear_settings(struct settings *settings)
@@ -632,6 +642,7 @@ void clear_settings(struct settings *settings)
 	free_regexes(&settings->include_regexes);
 	free_regexes(&settings->exclude_regexes);
 	free_env_vars(&settings->env_vars);
+	free_hook_strs(&settings->hook_strs);
 
 	init_settings(settings);
 }
@@ -641,6 +652,7 @@ bool parse_options(int argc, char **argv,
 {
 	int c;
 	char *env_test_root;
+	char *hook_str;
 
 	static struct option long_options[] = {
 		{"version", no_argument, NULL, OPT_VERSION},
@@ -751,15 +763,8 @@ bool parse_options(int argc, char **argv,
 			settings->code_coverage_script = bin_path(optarg);
 			break;
 		case OPT_HOOK:
-			/* FIXME: In order to allow line breaks, we should
-			 * change the format of settings serialization. Maybe
-			 * use JSON instead of our own format? */
-			if (strchr(optarg, '\n')) {
-				fprintf(stderr, "Newlines in --hook are currently unsupported.\n");
-				goto error;
-			}
-			/* FIXME: Allow as many options as allowed by test binaries. */
-			settings->hook_str = optarg;
+			hook_str = strdup(optarg);
+			igt_vec_push(&settings->hook_strs, &hook_str);
 			break;
 		case OPT_HELP_HOOK:
 			igt_hook_print_help(stdout, "--hook");
@@ -993,6 +998,49 @@ static bool serialize_environment(struct settings *settings, int dirfd)
 	return true;
 }
 
+static bool serialize_hook_strs(struct settings *settings, int dirfd)
+{
+	FILE *f;
+
+	if (file_exists_at(dirfd, hooks_filename) && !settings->overwrite) {
+		usage(stderr, "%s already exists, not overwriting", hooks_filename);
+		return false;
+	}
+
+	if ((f = fopenat_create(dirfd, hooks_filename, settings->overwrite)) == NULL)
+		return false;
+
+	for (size_t i = 0; i < igt_vec_length(&settings->hook_strs); i++) {
+		const char *s = *((char **)igt_vec_elem(&settings->hook_strs, i));
+		size_t len;
+
+		while (*s) {
+			len = strcspn(s, "\\\n");
+
+			if (len > 0)
+				fwrite(s, len, 1, f);
+
+			s += len;
+			if (!*s)
+				break;
+
+			fputc('\\', f);
+			fputc(*s, f);
+			s++;
+		}
+		fputc('\n', f);
+		fputc('\n', f);
+	}
+
+	if (settings->sync) {
+		fflush(f);
+		fsync(fileno(f));
+	}
+
+	fclose(f);
+	return true;
+}
+
 bool serialize_settings(struct settings *settings)
 {
 #define SERIALIZE_LINE(f, s, name, format) fprintf(f, "%s : " format "\n", #name, s->name)
@@ -1062,7 +1110,6 @@ bool serialize_settings(struct settings *settings)
 	SERIALIZE_LINE(f, settings, enable_code_coverage, "%d");
 	SERIALIZE_LINE(f, settings, cov_results_per_test, "%d");
 	SERIALIZE_LINE(f, settings, code_coverage_script, "%s");
-	SERIALIZE_LINE(f, settings, hook_str, "%s");
 
 	if (settings->sync) {
 		fflush(f);
@@ -1078,6 +1125,13 @@ bool serialize_settings(struct settings *settings)
 		}
 	}
 
+	if (igt_vec_length(&settings->hook_strs)) {
+		if (!serialize_hook_strs(settings, dirfd)) {
+			close(dirfd);
+			return false;
+		}
+	}
+
 	if (settings->sync)
 		fsync(dirfd);
 
@@ -1126,7 +1180,6 @@ bool read_settings_from_file(struct settings *settings, FILE *f)
 		PARSE_LINE(settings, name, val, enable_code_coverage, numval);
 		PARSE_LINE(settings, name, val, cov_results_per_test, numval);
 		PARSE_LINE(settings, name, val, code_coverage_script, val ? strdup(val) : NULL);
-		PARSE_LINE(settings, name, val, hook_str, val ? strdup(val) : NULL);
 
 		printf("Warning: Unknown field in settings file: %s = %s\n",
 		       name, val);
@@ -1196,6 +1249,82 @@ static bool read_env_vars_from_file(struct igt_list_head *env_vars, FILE *f)
 	return true;
 }
 
+static bool read_hook_strs_from_file(struct igt_vec *hook_strs, FILE *f)
+{
+	char *line = NULL;
+	ssize_t line_length;
+	size_t line_size = 0;
+	char *buf;
+	size_t buf_len = 0;
+	size_t buf_capacity = 128;
+
+	buf = malloc(buf_capacity);
+
+	while ((line_length = getline(&line, &line_size, f) != -1)) {
+		char *s = line;
+
+		if (buf_len == 0) {
+			while (isspace(*s)) {
+				line_length--;
+				s++;
+			}
+
+			if (*s == '\0' || *s == '#')
+				continue;
+		}
+
+		if (line_length + 1 > buf_capacity - buf_len) {
+			while (line_length + 1 > buf_capacity - buf_len)
+				buf_capacity *= 2;
+
+			buf = realloc(buf, buf_capacity);
+		}
+
+		while (true) {
+			if (*s == '\0' || *s == '\n') {
+				char *buf_copy;
+
+				if (!buf_len)
+					break;
+
+				/* Reached the end of a hook string. */
+				buf[buf_len] = '\0';
+				buf_copy = strdup(buf);
+				igt_vec_push(hook_strs, &buf_copy);
+				buf_len = 0;
+				break;
+			}
+
+			if (*s == '\\') {
+				s++;
+
+				if (*s == '\0')
+					/* Weird case of backslash being the
+					 * last character of the file. */
+					s--;
+			}
+
+			buf[buf_len++] = *s++;
+
+			if (*s == '\0' || *s == '\n')
+				break;
+		}
+	}
+
+	if (buf_len) {
+		char *buf_copy;
+
+		buf[buf_len] = '\0';
+		buf_copy = strdup(buf);
+		igt_vec_push(hook_strs, &buf_copy);
+	}
+
+	free(buf);
+	free(line);
+
+	return true;
+}
+
 bool read_settings_from_dir(struct settings *settings, int dirfd)
 {
 	FILE *f;
@@ -1226,5 +1355,18 @@ bool read_settings_from_dir(struct settings *settings, int dirfd)
 		fclose(f);
 	}
 
+	/* hooks file may not exist if no --hook was passed */
+	if (file_exists_at(dirfd, hooks_filename)) {
+		if ((f = fopenat_read(dirfd, hooks_filename)) == NULL)
+			return false;
+
+		if (!read_hook_strs_from_file(&settings->hook_strs, f)) {
+			fclose(f);
+			return false;
+		}
+
+		fclose(f);
+	}
+
 	return true;
 }
diff --git a/runner/settings.h b/runner/settings.h
index d3afb56de0393d58080ff8337b58180591ca5760..8335f0b8c8132b55b046ec32282ed2f0b80c7608 100644
--- a/runner/settings.h
+++ b/runner/settings.h
@@ -8,6 +8,7 @@
 #include <glib.h>
 
 #include "igt_list.h"
+#include "igt_vec.h"
 
 enum {
 	LOG_LEVEL_NORMAL = 0,
@@ -55,6 +56,7 @@ struct settings {
 	struct regex_list include_regexes;
 	struct regex_list exclude_regexes;
 	struct igt_list_head env_vars;
+	struct igt_vec hook_strs;
 	bool sync;
 	int log_level;
 	bool overwrite;
@@ -72,7 +74,6 @@ struct settings {
 	char *code_coverage_script;
 	bool enable_code_coverage;
 	bool cov_results_per_test;
-	char *hook_str;
 };
 
 /**