diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7943d52..346ce8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [Unreleased]
+### Changed
+- Allow `script` to be a single string value in the "common script schema"
+  which applies to hooks and aliases (#102)
+
 ### Fixed
 - Display nicer error message if no command is given and image doesn't specify a `Cmd` (#104)
 - Don't mangle && in scripts (#100)
diff --git a/doc/yaml-reference.md b/doc/yaml-reference.md
index 6adece5..c8021ed 100644
--- a/doc/yaml-reference.md
+++ b/doc/yaml-reference.md
@@ -54,7 +54,7 @@ hooks:
 
 ## Common script schema
 Several parts of `.scuba.yml` which define "scripts" use a common schema.
-The *common script schema* can define a "script" in one of two forms:
+The *common script schema* can define a "script" in one of several forms:
 
 The *simple* form is simply a single string value:
 ```yaml
@@ -63,7 +63,14 @@ hooks:
 ```
 
 The *complex* form is a mapping, which must contain a `script` subkey, whose
-value is a list of strings making up the script:
+value is either single string value
+```yaml
+hooks:
+  root:
+    script: echo hello
+```
+
+... or a list of strings making up the script:
 ```yaml
 hooks:
   root:
diff --git a/example/per_alias_image/.scuba.yml b/example/per_alias_image/.scuba.yml
index 9b9bae0..df73881 100644
--- a/example/per_alias_image/.scuba.yml
+++ b/example/per_alias_image/.scuba.yml
@@ -2,11 +2,12 @@ image: !from_yaml ../common.yml image
 aliases:
 
   # This one inherits the default, top-level 'image'
+  # and specifies "script" as a string
   default:
-    script:
-      - cat /etc/os-release
+    script: cat /etc/os-release
 
   # This one specifies a different image to use
+  # and specifies "script" as a list
   different:
     image: alpine
     script:
diff --git a/scuba/config.py b/scuba/config.py
index d1d33eb..b8e0417 100644
--- a/scuba/config.py
+++ b/scuba/config.py
@@ -118,10 +118,13 @@ def _process_script_node(node, name):
         if not script:
             raise ConfigError("{0}: must have a 'script' subkey".format(name))
 
-        if not isinstance(script, list):
-            raise ConfigError("{0}.script: must be a list".format(name))
+        if isinstance(script, list):
+            return script
 
-        return script
+        if isinstance(script, basestring):
+            return [script]
+
+        raise ConfigError("{0}.script: must be a string or list".format(name))
 
     raise ConfigError("{0}: must be string or dict".format(name))
 

Allow `script` to be a single string value
Currently, this is invalid:
```yaml
aliases:
  foo:
    image: xyz
    script: echo Hello
```
diff --git a/tests/test_config.py b/tests/test_config.py
index 8e4d7c9..118379e 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -8,26 +8,52 @@ from unittest import TestCase
 import logging
 import os
 from os.path import join
-from tempfile import mkdtemp
 from shutil import rmtree
 
 from scuba.utils import shlex_split
 import scuba.config
 
-class TestConfig(TestCase):
-    def setUp(self):
-        self.orig_path = os.getcwd()
 
-        self.path = mkdtemp('scubatest')
-        os.chdir(self.path)
-        logging.info('Temp path: ' + self.path)
-
-    def tearDown(self):
-        rmtree(self.path)
-        self.path = None
-
-        os.chdir(self.orig_path)
-        self.orig_path = None
+class TestCommonScriptSchema(TmpDirTestCase):
+    def test_simple(self):
+        '''Simple form: value is a string'''
+        node = 'foo'
+        result = scuba.config._process_script_node(node, 'dontcare')
+        assert_equals(result, ['foo'])
+
+    def test_script_key_string(self):
+        '''Value is a mapping: script is a string'''
+        node = dict(
+            script = 'foo',
+            otherkey = 'other',
+        )
+        result = scuba.config._process_script_node(node, 'dontcare')
+        assert_equals(result, ['foo'])
+
+    def test_script_key_list(self):
+        '''Value is a mapping: script is a list'''
+        node = dict(
+            script = [
+                'foo',
+                'bar',
+            ],
+            otherkey = 'other',
+        )
+        result = scuba.config._process_script_node(node, 'dontcare')
+        assert_equals(result, ['foo', 'bar'])
+
+    def test_script_key_mapping_invalid(self):
+        '''Value is a mapping: script is a mapping (invalid)'''
+        node = dict(
+            script = dict(
+                whatisthis = 'idontknow',
+            ),
+        )
+        assert_raises(scuba.config.ConfigError,
+                scuba.config._process_script_node, node, 'dontcare')
+
+
+class TestConfig(TmpDirTestCase):
 
     ######################################################################
     # Find config
@@ -335,19 +361,6 @@ class TestConfig(TestCase):
 
         assert_raises(scuba.config.ConfigError, scuba.config.load_config, '.scuba.yml')
 
-    # TODO: Any reason this shouldn't be valid?
-    def test_hooks_invalid_script_type(self):
-        '''hooks with string "script" are invalid'''
-        with open('.scuba.yml', 'w') as f:
-            f.write('''
-                image: na
-                hooks:
-                  user:
-                    script: this should be in a list under script
-                ''')
-
-        assert_raises(scuba.config.ConfigError, scuba.config.load_config, '.scuba.yml')
-
     def test_hooks_missing_script(self):
         '''hooks with dict, but missing "script" are invalid'''
         with open('.scuba.yml', 'w') as f:
diff --git a/tests/test_main.py b/tests/test_main.py
index 00f8d4b..588d0e9 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -11,8 +11,7 @@ except ImportError:
 import logging
 import os
 import sys
-from tempfile import mkdtemp, TemporaryFile, NamedTemporaryFile
-from shutil import rmtree
+from tempfile import TemporaryFile, NamedTemporaryFile
 import subprocess
 
 import scuba.__main__ as main
@@ -22,22 +21,7 @@ import scuba
 
 DOCKER_IMAGE = 'debian:8.2'
 
-class TestMain(TestCase):
-    def setUp(self):
-        # Run each test in its own temp directory
-        self.orig_path = os.getcwd()
-        self.path = mkdtemp('scubatest')
-        os.chdir(self.path)
-        logging.info('Temp path: ' + self.path)
-
-
-    def tearDown(self):
-        # Restore the working dir and cleanup the temp one
-        rmtree(self.path)
-        self.path = None
-        os.chdir(self.orig_path)
-        self.orig_path = None
-
+class TestMain(TmpDirTestCase):
 
     def run_scuba(self, args, exp_retval=0, mock_isatty=False, stdin=None):
         '''Run scuba, checking its return value
diff --git a/tests/utils.py b/tests/utils.py
index 1dfaa74..1444b3d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,6 +4,8 @@ from nose.tools import *
 from os.path import normpath
 import tempfile
 import shutil
+import unittest
+import logging
 
 def assert_set_equal(a, b):
     assert_equal(set(a), set(b))
@@ -100,3 +102,20 @@ class RedirStd(object):
 
         if self.orig_stderr:
             sys.stderr = self.orig_stderr
+
+
+class TmpDirTestCase(unittest.TestCase):
+    def setUp(self):
+        # Run each test in its own temp directory
+        self.orig_path = os.getcwd()
+        self.path = tempfile.mkdtemp('scubatest')
+        os.chdir(self.path)
+        logging.info('Temp path: ' + self.path)
+
+
+    def tearDown(self):
+        # Restore the working dir and cleanup the temp one
+        shutil.rmtree(self.path)
+        self.path = None
+        os.chdir(self.orig_path)
+        self.orig_path = None

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc g++ make
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==3.7.1
exceptiongroup==1.2.2
iniconfig==2.1.0
nose==1.3.7
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
PyYAML==6.0.2
-e git+https://github.com/JonathonReinhart/scuba.git@b323aff30dc09cb16bb19eca41a0c2d15d9a22b6#egg=scuba
tomli==2.2.1

name: scuba
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==3.7.1
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - nose==1.3.7
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pyyaml==6.0.2
      - tomli==2.2.1
prefix: /opt/conda/envs/scuba

tests/test_config.py::TestCommonScriptSchema::test_script_key_string
tests/test_main.py::TestMain::test_arbitrary_docker_args tests/test_main.py::TestMain::test_args tests/test_main.py::TestMain::test_basic tests/test_main.py::TestMain::test_complex_commands_in_alias tests/test_main.py::TestMain::test_created_file_ownership tests/test_main.py::TestMain::test_dry_run tests/test_main.py::TestMain::test_home_writable_root tests/test_main.py::TestMain::test_home_writable_scubauser tests/test_main.py::TestMain::test_image_entrypoint tests/test_main.py::TestMain::test_image_override tests/test_main.py::TestMain::test_image_override_with_alias tests/test_main.py::TestMain::test_list_aliases tests/test_main.py::TestMain::test_no_cmd tests/test_main.py::TestMain::test_no_docker tests/test_main.py::TestMain::test_redirect_stdin tests/test_main.py::TestMain::test_root_hook tests/test_main.py::TestMain::test_user_hook tests/test_main.py::TestMain::test_user_root tests/test_main.py::TestMain::test_user_scubauser tests/test_main.py::TestMain::test_with_tty tests/test_main.py::TestMain::test_without_tty tests/test_main.py::TestMain::test_yml_not_needed_with_image_override
tests/test_config.py::TestCommonScriptSchema::test_script_key_list tests/test_config.py::TestCommonScriptSchema::test_script_key_mapping_invalid tests/test_config.py::TestCommonScriptSchema::test_simple tests/test_config.py::TestConfig::test_find_config_cur_dir tests/test_config.py::TestConfig::test_find_config_nonexist tests/test_config.py::TestConfig::test_find_config_parent_dir tests/test_config.py::TestConfig::test_find_config_way_up tests/test_config.py::TestConfig::test_hooks_invalid_list tests/test_config.py::TestConfig::test_hooks_missing_script tests/test_config.py::TestConfig::test_hooks_mixed tests/test_config.py::TestConfig::test_load_config__no_spaces_in_aliases tests/test_config.py::TestConfig::test_load_config_empty tests/test_config.py::TestConfig::test_load_config_image_from_yaml tests/test_config.py::TestConfig::test_load_config_image_from_yaml_missing_arg tests/test_config.py::TestConfig::test_load_config_image_from_yaml_missing_file tests/test_config.py::TestConfig::test_load_config_image_from_yaml_nested_key_missing tests/test_config.py::TestConfig::test_load_config_image_from_yaml_nested_keys tests/test_config.py::TestConfig::test_load_config_image_from_yaml_unicode_args tests/test_config.py::TestConfig::test_load_config_minimal tests/test_config.py::TestConfig::test_load_config_with_aliases tests/test_config.py::TestConfig::test_load_unexpected_node tests/test_config.py::TestConfig::test_process_command_alias_overrides_image tests/test_config.py::TestConfig::test_process_command_aliases_unused tests/test_config.py::TestConfig::test_process_command_aliases_used_noargs tests/test_config.py::TestConfig::test_process_command_aliases_used_withargs tests/test_config.py::TestConfig::test_process_command_empty tests/test_config.py::TestConfig::test_process_command_multiline_aliases_forbid_user_args tests/test_config.py::TestConfig::test_process_command_multiline_aliases_used tests/test_config.py::TestConfig::test_process_command_no_aliases tests/test_main.py::TestMain::test_config_error tests/test_main.py::TestMain::test_handle_get_image_command_error tests/test_main.py::TestMain::test_multiline_alias_no_args_error tests/test_main.py::TestMain::test_no_image_cmd tests/test_main.py::TestMain::test_version
example/per_alias_image/.scuba.yml doc/yaml-reference.md scuba/config.py CHANGELOG.md
example/per_alias_image/.scuba.yml doc/yaml-reference.md scuba/config.py CHANGELOG.md