diff --git a/bisect.c b/bisect.c
index fd581b85a72..415b0e7b0b2 100644
--- a/bisect.c
+++ b/bisect.c
@@ -738,18 +738,17 @@ enum bisect_error bisect_checkout(const struct object_id *bisect_rev,
 	update_ref(NULL, "BISECT_EXPECTED_REV", bisect_rev, NULL, 0, UPDATE_REFS_DIE_ON_ERR);
 
 	argv_checkout[2] = bisect_rev_hex;
-	if (no_checkout) {
+	if (no_checkout)
 		update_ref(NULL, "BISECT_HEAD", bisect_rev, NULL, 0,
 			   UPDATE_REFS_DIE_ON_ERR);
-	} else {
-		if (run_command_v_opt(argv_checkout, RUN_GIT_CMD))
-			/*
-			 * Errors in `run_command()` itself, signaled by res < 0,
-			 * and errors in the child process, signaled by res > 0
-			 * can both be treated as regular BISECT_FAILED (-1).
-			 */
-			return BISECT_FAILED;
-	}
+	else if (run_command_l_opt(RUN_GIT_CMD, "checkout", "-q",
+				   bisect_rev_hex, "--", NULL))
+		/*
+		 * Errors in `run_command()` itself, signaled by res < 0,
+		 * and errors in the child process, signaled by res > 0
+		 * can both be treated as regular BISECT_FAILED (-1).
+		 */
+		return BISECT_FAILED;
 
 	commit = lookup_commit_reference(the_repository, bisect_rev);
 	format_commit_message(commit, "[%H] %s%n", &commit_msg, &pp);
diff --git a/builtin/clone.c b/builtin/clone.c
index ed8d44bb6ab..13fc7a4bc5d 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -651,23 +651,19 @@ static void update_head(const struct ref *our, const struct ref *remote,
 
 static int git_sparse_checkout_init(const char *repo)
 {
-	struct strvec argv = STRVEC_INIT;
-	int result = 0;
-	strvec_pushl(&argv, "-C", repo, "sparse-checkout", "set", NULL);
-
 	/*
 	 * We must apply the setting in the current process
 	 * for the later checkout to use the sparse-checkout file.
 	 */
 	core_apply_sparse_checkout = 1;
 
-	if (run_command_v_opt(argv.v, RUN_GIT_CMD)) {
+	if (run_command_l_opt(RUN_GIT_CMD, "-C", repo, "sparse-checkout",
+			      "set", NULL)) {
 		error(_("failed to initialize sparse-checkout"));
-		result = 1;
+		return 1;
 	}
 
-	strvec_clear(&argv);
-	return result;
+	return 0;
 }
 
 static int checkout(int submodule_progress, int filter_submodules)
@@ -862,11 +858,11 @@ static void write_refspec_config(const char *src_ref_prefix,
 
 static void dissociate_from_references(void)
 {
-	static const char* argv[] = { "repack", "-a", "-d", NULL };
 	char *alternates = git_pathdup("objects/info/alternates");
 
 	if (!access(alternates, F_OK)) {
-		if (run_command_v_opt(argv, RUN_GIT_CMD|RUN_COMMAND_NO_STDIN))
+		if (run_command_l_opt(RUN_GIT_CMD|RUN_COMMAND_NO_STDIN,
+				      "repack",  "-a", "-d", NULL))
 			die(_("cannot repack to clean up"));
 		if (unlink(alternates) && errno != ENOENT)
 			die_errno(_("cannot unlink temporary alternates file"));
diff --git a/builtin/difftool.c b/builtin/difftool.c
index 4b10ad1a369..ed211a87322 100644
--- a/builtin/difftool.c
+++ b/builtin/difftool.c
@@ -44,8 +44,8 @@ static int difftool_config(const char *var, const char *value, void *cb)
 
 static int print_tool_help(void)
 {
-	const char *argv[] = { "mergetool", "--tool-help=diff", NULL };
-	return run_command_v_opt(argv, RUN_GIT_CMD);
+	return run_command_l_opt(RUN_GIT_CMD, "mergetool", "--tool-help=diff",
+				 NULL);
 }
 
 static int parse_index_info(char *p, int *mode1, int *mode2,
@@ -361,7 +361,7 @@ static int run_dir_diff(const char *extcmd, int symlinks, const char *prefix,
 	struct index_state wtindex;
 	struct checkout lstate, rstate;
 	int flags = RUN_GIT_CMD, err = 0;
-	const char *helper_argv[] = { "difftool--helper", NULL, NULL, NULL };
+	const char *helper_command = "difftool--helper";
 	struct hashmap wt_modified, tmp_modified;
 	int indices_loaded = 0;
 
@@ -563,16 +563,14 @@ static int run_dir_diff(const char *extcmd, int symlinks, const char *prefix,
 	}
 
 	strbuf_setlen(&ldir, ldir_len);
-	helper_argv[1] = ldir.buf;
 	strbuf_setlen(&rdir, rdir_len);
-	helper_argv[2] = rdir.buf;
-
 	if (extcmd) {
-		helper_argv[0] = extcmd;
+		helper_command = extcmd;
 		flags = 0;
 	} else
 		setenv("GIT_DIFFTOOL_DIRDIFF", "true", 1);
-	ret = run_command_v_opt(helper_argv, flags);
+	ret = run_command_l_opt(flags, helper_command, ldir.buf, rdir.buf,
+				NULL);
 
 	/* TODO: audit for interaction with sparse-index. */
 	ensure_full_index(&wtindex);
diff --git a/builtin/gc.c b/builtin/gc.c
index 243ee85d283..075b4637660 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -58,8 +58,6 @@ static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE;
 static struct strvec reflog = STRVEC_INIT;
 static struct strvec repack = STRVEC_INIT;
 static struct strvec prune = STRVEC_INIT;
-static struct strvec prune_worktrees = STRVEC_INIT;
-static struct strvec rerere = STRVEC_INIT;
 
 static struct tempfile *pidfile;
 static struct lock_file log_lock;
@@ -167,9 +165,8 @@ static void gc_config(void)
 struct maintenance_run_opts;
 static int maintenance_task_pack_refs(MAYBE_UNUSED struct maintenance_run_opts *opts)
 {
-	const char *argv[] = { "pack-refs", "--all", "--prune", NULL };
-
-	return run_command_v_opt(argv, RUN_GIT_CMD);
+	return run_command_l_opt(RUN_GIT_CMD, "pack-refs", "--all", "--prune",
+				 NULL);
 }
 
 static int too_many_loose_objects(void)
@@ -574,8 +571,6 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	strvec_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	strvec_pushl(&repack, "repack", "-d", "-l", NULL);
 	strvec_pushl(&prune, "prune", "--expire", NULL);
-	strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL);
-	strvec_pushl(&rerere, "rerere", "gc", NULL);
 
 	/* default expiry time, overwritten in gc_config */
 	gc_config();
@@ -688,14 +683,13 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 		}
 	}
 
-	if (prune_worktrees_expire) {
-		strvec_push(&prune_worktrees, prune_worktrees_expire);
-		if (run_command_v_opt(prune_worktrees.v, RUN_GIT_CMD))
-			die(FAILED_RUN, prune_worktrees.v[0]);
-	}
+	if (prune_worktrees_expire &&
+	    run_command_l_opt(RUN_GIT_CMD, "worktree", "prune", "--expire",
+			      prune_worktrees_expire, NULL))
+		die(FAILED_RUN, "worktree");
 
-	if (run_command_v_opt(rerere.v, RUN_GIT_CMD))
-		die(FAILED_RUN, rerere.v[0]);
+	if (run_command_l_opt(RUN_GIT_CMD, "rerere", "gc", NULL))
+		die(FAILED_RUN, "rerere");
 
 	report_garbage = report_pack_garbage;
 	reprepare_packed_git(the_repository);
diff --git a/builtin/merge.c b/builtin/merge.c
index 3bb49d805b4..9c04b588f68 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -347,55 +347,32 @@ static int save_state(struct object_id *stash)
 
 static void read_empty(const struct object_id *oid)
 {
-	int i = 0;
-	const char *args[7];
-
-	args[i++] = "read-tree";
-	args[i++] = "-m";
-	args[i++] = "-u";
-	args[i++] = empty_tree_oid_hex();
-	args[i++] = oid_to_hex(oid);
-	args[i] = NULL;
-
-	if (run_command_v_opt(args, RUN_GIT_CMD))
+	if (run_command_l_opt(RUN_GIT_CMD, "read-tree", "-m", "-u",
+			      empty_tree_oid_hex(), oid_to_hex(oid), NULL))
 		die(_("read-tree failed"));
 }
 
 static void reset_hard(const struct object_id *oid)
 {
-	int i = 0;
-	const char *args[6];
-
-	args[i++] = "read-tree";
-	args[i++] = "-v";
-	args[i++] = "--reset";
-	args[i++] = "-u";
-	args[i++] = oid_to_hex(oid);
-	args[i] = NULL;
-
-	if (run_command_v_opt(args, RUN_GIT_CMD))
+	if (run_command_l_opt(RUN_GIT_CMD, "read-tree", "-v", "--reset", "-u",
+			      oid_to_hex(oid), NULL))
 		die(_("read-tree failed"));
 }
 
 static void restore_state(const struct object_id *head,
 			  const struct object_id *stash)
 {
-	struct strvec args = STRVEC_INIT;
-
 	reset_hard(head);
 
 	if (is_null_oid(stash))
 		goto refresh_cache;
 
-	strvec_pushl(&args, "stash", "apply", "--index", "--quiet", NULL);
-	strvec_push(&args, oid_to_hex(stash));
-
 	/*
 	 * It is OK to ignore error here, for example when there was
 	 * nothing to restore.
 	 */
-	run_command_v_opt(args.v, RUN_GIT_CMD);
-	strvec_clear(&args);
+	run_command_l_opt(RUN_GIT_CMD, "stash", "apply", "--index", "--quiet",
+			  oid_to_hex(stash), NULL);
 
 refresh_cache:
 	if (discard_cache() < 0 || read_cache() < 0)
diff --git a/builtin/remote.c b/builtin/remote.c
index 910f7b9316a..1d86c14297b 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -92,13 +92,11 @@ static int verbose;
 
 static int fetch_remote(const char *name)
 {
-	const char *argv[] = { "fetch", name, NULL, NULL };
-	if (verbose) {
-		argv[1] = "-v";
-		argv[2] = name;
-	}
 	printf_ln(_("Updating %s"), name);
-	if (run_command_v_opt(argv, RUN_GIT_CMD))
+	if (verbose && run_command_l_opt(RUN_GIT_CMD, "-v", "fetch", name,
+					 NULL))
+		return error(_("Could not fetch %s"), name);
+	else if (run_command_l_opt(RUN_GIT_CMD, "fetch", name, NULL))
 		return error(_("Could not fetch %s"), name);
 	return 0;
 }
diff --git a/compat/mingw.c b/compat/mingw.c
index 901375d5841..4f5392c5796 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -196,17 +196,15 @@ static int read_yes_no_answer(void)
 static int ask_yes_no_if_possible(const char *format, ...)
 {
 	char question[4096];
-	const char *retry_hook[] = { NULL, NULL, NULL };
+	char *retry_hook;
 	va_list args;
 
 	va_start(args, format);
 	vsnprintf(question, sizeof(question), format, args);
 	va_end(args);
 
-	if ((retry_hook[0] = mingw_getenv("GIT_ASK_YESNO"))) {
-		retry_hook[1] = question;
-		return !run_command_v_opt(retry_hook, 0);
-	}
+	if ((retry_hook = mingw_getenv("GIT_ASK_YESNO")))
+		return !run_command_l_opt(0, retry_hook, question, NULL);
 
 	if (!isatty(_fileno(stdin)) || !isatty(_fileno(stderr)))
 		return 0;
diff --git a/ll-merge.c b/ll-merge.c
index 8955d7e1f6e..740689b7bd6 100644
--- a/ll-merge.c
+++ b/ll-merge.c
@@ -193,7 +193,6 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
 	struct strbuf cmd = STRBUF_INIT;
 	struct strbuf_expand_dict_entry dict[6];
 	struct strbuf path_sq = STRBUF_INIT;
-	const char *args[] = { NULL, NULL };
 	int status, fd, i;
 	struct stat st;
 	enum ll_merge_result ret;
@@ -219,8 +218,7 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
 
 	strbuf_expand(&cmd, fn->cmdline, strbuf_expand_dict_cb, &dict);
 
-	args[0] = cmd.buf;
-	status = run_command_v_opt(args, RUN_USING_SHELL);
+	status = run_command_l_opt(RUN_USING_SHELL, cmd.buf, NULL);
 	fd = open(temp[1], O_RDONLY);
 	if (fd < 0)
 		goto bad;
diff --git a/run-command.c b/run-command.c
index cf4a431c839..0be5626f7f2 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1016,6 +1016,21 @@ static void run_command_set_opts(struct child_process *cmd, int opt)
 	cmd->close_object_store = opt & RUN_CLOSE_OBJECT_STORE ? 1 : 0;
 }
 
+int run_command_l_opt(int opt, ...)
+{
+	struct child_process cmd = CHILD_PROCESS_INIT;
+	va_list ap;
+	const char *arg;
+
+	va_start(ap, opt);
+	while ((arg = va_arg(ap, const char *)))
+		strvec_push(&cmd.args, arg);
+	va_end(ap);
+
+	run_command_set_opts(&cmd, opt);
+	return run_command(&cmd);
+}
+
 int run_command_v_opt(const char **argv, int opt)
 {
 	return run_command_v_opt_cd_env(argv, opt, NULL, NULL);
diff --git a/run-command.h b/run-command.h
index 0e85e5846a5..6320d70f062 100644
--- a/run-command.h
+++ b/run-command.h
@@ -151,8 +151,8 @@ struct child_process {
 
 /**
  * The functions: child_process_init, start_command, finish_command,
- * run_command, run_command_v_opt, run_command_v_opt_cd_env, child_process_clear
- * do the following:
+ * run_command, run_command_l_opt, run_command_v_opt,
+ * run_command_v_opt_cd_env, child_process_clear do the following:
  *
  * - If a system call failed, errno is set and -1 is returned. A diagnostic
  *   is printed.
@@ -254,6 +254,15 @@ int run_command_v_opt_cd_env(const char **argv, int opt, const char *dir, const
 int run_command_v_opt_cd_env_tr2(const char **argv, int opt, const char *dir,
 				 const char *const *env, const char *tr2_class);
 
+/**
+ * The run_command_l_opt() function run_command_v_opt() takes a list
+ * of strings terminated by a NULL. Use it instead of
+ * run_command_v_opt() when possible, as it allows the compiler to
+ * check that the "argv" is NULL-delimited.
+ */
+LAST_ARG_MUST_BE_NULL
+int run_command_l_opt(int opt, ...);
+
 /**
  * Execute the given command, sending "in" to its stdin, and capturing its
  * stdout and stderr in the "out" and "err" strbufs. Any of the three may
diff --git a/sequencer.c b/sequencer.c
index debb2ecbafe..20495db9de2 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -3558,12 +3558,10 @@ static int error_failed_squash(struct repository *r,
 
 static int do_exec(struct repository *r, const char *command_line)
 {
-	const char *child_argv[] = { NULL, NULL };
 	int dirty, status;
 
 	fprintf(stderr, _("Executing: %s\n"), command_line);
-	child_argv[0] = command_line;
-	status = run_command_v_opt(child_argv, RUN_USING_SHELL);
+	status = run_command_l_opt(RUN_USING_SHELL, command_line, NULL);
 
 	/* force re-reading of the cache */
 	if (discard_index(r->index) < 0 || repo_read_index(r) < 0)
diff --git a/t/helper/test-fake-ssh.c b/t/helper/test-fake-ssh.c
index 12beee99ad2..7967478a40a 100644
--- a/t/helper/test-fake-ssh.c
+++ b/t/helper/test-fake-ssh.c
@@ -8,7 +8,6 @@ int cmd_main(int argc, const char **argv)
 	struct strbuf buf = STRBUF_INIT;
 	FILE *f;
 	int i;
-	const char *child_argv[] = { NULL, NULL };
 
 	/* First, print all parameters into $TRASH_DIRECTORY/ssh-output */
 	if (!trash_directory)
@@ -25,6 +24,5 @@ int cmd_main(int argc, const char **argv)
 	/* Now, evaluate the *last* parameter */
 	if (argc < 2)
 		return 0;
-	child_argv[0] = argv[argc - 1];
-	return run_command_v_opt(child_argv, RUN_USING_SHELL);
+	return run_command_l_opt(RUN_USING_SHELL, argv[argc - 1], NULL);
 }
