diff --git a/.circleci/test_pytest.sh b/.circleci/test_pytest.sh
index 832a52a02b..e551ae6d85 100644
--- a/.circleci/test_pytest.sh
+++ b/.circleci/test_pytest.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
 
-docker run --rm=false -t -v $WORKDIR:/work -v $HOME/examples:/data/examples:ro -w /work -e CI_SKIP_TEST=1 -e NIPYPE_RESOURCE_MONITOR=1 "${DOCKER_IMAGE}:py38" /usr/bin/run_pytests.sh
+docker run --rm=false -t -v "$WORKDIR":/work -v "$HOME"/examples:/data/examples:ro -w /work -e CI_SKIP_TEST=1 -e NIPYPE_RESOURCE_MONITOR=1 "${DOCKER_IMAGE}:py38" /usr/bin/run_pytests.sh
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
new file mode 100644
index 0000000000..087aba7689
--- /dev/null
+++ b/.github/workflows/shellcheck.yml
@@ -0,0 +1,23 @@
+---
+name: Shellcheck
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches: [master]
+
+jobs:
+  shellcheck:
+    name: Check shell scripts
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Install dependencies
+        run: |
+          sudo apt update && sudo apt install -y shellcheck
+      - name: shellcheck
+        run: |
+          git grep -l '^#\( *shellcheck \|!\(/bin/\|/usr/bin/env \)\(sh\|bash\|dash\|ksh\)\)' | xargs shellcheck 
diff --git a/docker/files/run_builddocs.sh b/docker/files/run_builddocs.sh
index fb6111c9ea..8f6510a7bd 100644
--- a/docker/files/run_builddocs.sh
+++ b/docker/files/run_builddocs.sh
@@ -5,7 +5,7 @@ set -u
 
 WORKDIR=${WORK:-/work}
 
-mkdir -p ${WORKDIR}/docs
-make html 2>&1 | tee ${WORKDIR}/builddocs.log
-cp -r /src/nipype/doc/_build/html/* ${WORKDIR}/docs/
-cat ${WORKDIR}/builddocs.log && if grep -q "ERROR" ${WORKDIR}/builddocs.log; then false; else true; fi
+mkdir -p "${WORKDIR}"/docs
+make html 2>&1 | tee "${WORKDIR}"/builddocs.log
+cp -r /src/nipype/doc/_build/html/* "${WORKDIR}"/docs/
+cat "${WORKDIR}"/builddocs.log && if grep -q "ERROR" "${WORKDIR}"/builddocs.log; then false; else true; fi
diff --git a/docker/files/run_examples.sh b/docker/files/run_examples.sh
index 1914f53be9..54207340ac 100644
--- a/docker/files/run_examples.sh
+++ b/docker/files/run_examples.sh
@@ -8,21 +8,21 @@ arr=$@
 tmp_var=$( IFS=$' '; echo "${arr[*]}" )
 example_id=${tmp_var//[^A-Za-z0-9_-]/_}
 
-mkdir -p ${HOME}/.nipype ${WORKDIR}/logs/example_${example_id} ${WORKDIR}/tests ${WORKDIR}/crashfiles
-echo "[logging]" > ${HOME}/.nipype/nipype.cfg
-echo "workflow_level = DEBUG" >> ${HOME}/.nipype/nipype.cfg
-echo "interface_level = DEBUG" >> ${HOME}/.nipype/nipype.cfg
-echo "utils_level = DEBUG" >> ${HOME}/.nipype/nipype.cfg
-echo "log_to_file = true" >> ${HOME}/.nipype/nipype.cfg
-echo "log_directory = ${WORKDIR}/logs/example_${example_id}" >> ${HOME}/.nipype/nipype.cfg
+mkdir -p "${HOME}"/.nipype "${WORKDIR}"/logs/example_"${example_id}" "${WORKDIR}"/tests "${WORKDIR}"/crashfiles
+echo "[logging]" > "${HOME}"/.nipype/nipype.cfg
+echo "workflow_level = DEBUG" >> "${HOME}"/.nipype/nipype.cfg
+echo "interface_level = DEBUG" >> "${HOME}"/.nipype/nipype.cfg
+echo "utils_level = DEBUG" >> "${HOME}"/.nipype/nipype.cfg
+echo "log_to_file = true" >> "${HOME}"/.nipype/nipype.cfg
+echo "log_directory = ${WORKDIR}/logs/example_${example_id}" >> "${HOME}"/.nipype/nipype.cfg
 
-echo '[execution]' >> ${HOME}/.nipype/nipype.cfg
-echo 'crashfile_format = txt' >> ${HOME}/.nipype/nipype.cfg
+echo '[execution]' >> "${HOME}"/.nipype/nipype.cfg
+echo 'crashfile_format = txt' >> "${HOME}"/.nipype/nipype.cfg
 
 if [[ "${NIPYPE_RESOURCE_MONITOR:-0}" == "1" ]]; then
-    echo '[monitoring]' >> ${HOME}/.nipype/nipype.cfg
-    echo 'enabled = true' >> ${HOME}/.nipype/nipype.cfg
-    echo 'sample_frequency = 3' >> ${HOME}/.nipype/nipype.cfg
+    echo '[monitoring]' >> "${HOME}"/.nipype/nipype.cfg
+    echo 'enabled = true' >> "${HOME}"/.nipype/nipype.cfg
+    echo 'sample_frequency = 3' >> "${HOME}"/.nipype/nipype.cfg
 fi
 
 # Set up coverage
@@ -35,9 +35,9 @@ coverage run /src/nipype/tools/run_examples.py $@
 exit_code=$?
 
 if [[ "${NIPYPE_RESOURCE_MONITOR:-0}" == "1" ]]; then
-	cp resource_monitor.json 2>/dev/null ${WORKDIR}/logs/example_${example_id}/ || :
+	cp resource_monitor.json 2>/dev/null "${WORKDIR}"/logs/example_"${example_id}"/ || :
 fi
 # Collect crashfiles and generate xml report
-coverage xml -o ${WORKDIR}/tests/smoketest_${example_id}.xml
-find /work -maxdepth 1 -name "crash-*" -exec mv {} ${WORKDIR}/crashfiles/ \;
+coverage xml -o "${WORKDIR}"/tests/smoketest_"${example_id}".xml
+find /work -maxdepth 1 -name "crash-*" -exec mv {} "${WORKDIR}"/crashfiles/ \;
 exit $exit_code
diff --git a/docker/files/run_pytests.sh b/docker/files/run_pytests.sh
index c3d33f2f53..a35c11d4f5 100644
--- a/docker/files/run_pytests.sh
+++ b/docker/files/run_pytests.sh
@@ -9,32 +9,32 @@ WORKDIR=${WORK:-/work}
 PYTHON_VERSION=$( python -c "import sys; print('{}{}'.format(sys.version_info[0], sys.version_info[1]))" )
 
 # Create necessary directories
-mkdir -p ${WORKDIR}/tests ${WORKDIR}/crashfiles ${WORKDIR}/logs/py${PYTHON_VERSION}
+mkdir -p "${WORKDIR}"/tests "${WORKDIR}"/crashfiles "${WORKDIR}"/logs/py"${PYTHON_VERSION}"
 
 # Create a nipype config file
-mkdir -p ${HOME}/.nipype
-echo '[logging]' > ${HOME}/.nipype/nipype.cfg
-echo 'log_to_file = true' >> ${HOME}/.nipype/nipype.cfg
-echo "log_directory = ${WORKDIR}/logs/py${PYTHON_VERSION}" >> ${HOME}/.nipype/nipype.cfg
+mkdir -p "${HOME}"/.nipype
+echo '[logging]' > "${HOME}"/.nipype/nipype.cfg
+echo 'log_to_file = true' >> "${HOME}"/.nipype/nipype.cfg
+echo "log_directory = ${WORKDIR}/logs/py${PYTHON_VERSION}" >> "${HOME}"/.nipype/nipype.cfg
 
-echo '[execution]' >> ${HOME}/.nipype/nipype.cfg
-echo 'crashfile_format = txt' >> ${HOME}/.nipype/nipype.cfg
+echo '[execution]' >> "${HOME}"/.nipype/nipype.cfg
+echo 'crashfile_format = txt' >> "${HOME}"/.nipype/nipype.cfg
 
 if [[ "${NIPYPE_RESOURCE_MONITOR:-0}" == "1" ]]; then
-    echo 'resource_monitor = true' >> ${HOME}/.nipype/nipype.cfg
+    echo 'resource_monitor = true' >> "${HOME}"/.nipype/nipype.cfg
 fi
 
 # Run tests using pytest
 export COVERAGE_FILE=${WORKDIR}/tests/.coverage.py${PYTHON_VERSION}
-py.test -v --junitxml=${WORKDIR}/tests/pytests_py${PYTHON_VERSION}.xml \
+py.test -v --junitxml="${WORKDIR}"/tests/pytests_py"${PYTHON_VERSION}".xml \
     --cov nipype --cov-config /src/nipype/.coveragerc \
-    --cov-report xml:${WORKDIR}/tests/coverage_py${PYTHON_VERSION}.xml \
+    --cov-report xml:"${WORKDIR}"/tests/coverage_py"${PYTHON_VERSION}".xml \
     -n auto \
-    -c ${TESTPATH}/pytest.ini ${TESTPATH}
+    -c "${TESTPATH}"/pytest.ini "${TESTPATH}"
 exit_code=$?
 
 # Collect crashfiles
-find ${WORKDIR} -maxdepth 1 -name "crash-*" -exec mv {} ${WORKDIR}/crashfiles/ \;
+find "${WORKDIR}" -maxdepth 1 -name "crash-*" -exec mv {} "${WORKDIR}"/crashfiles/ \;
 
 echo "Unit tests finished with exit code ${exit_code}"
 exit ${exit_code}
diff --git a/docker/generate_dockerfiles.sh b/docker/generate_dockerfiles.sh
index a2982bb003..441b353a92 100755
--- a/docker/generate_dockerfiles.sh
+++ b/docker/generate_dockerfiles.sh
@@ -4,7 +4,7 @@
 
 set -e
 
-USAGE="usage: $(basename $0) [-h] [-b] [-m]"
+USAGE="usage: $(basename "$0") [-h] [-b] [-m]"
 
 function Help {
   cat <<USAGE
@@ -12,7 +12,7 @@ Generate base and/or main Dockerfiles for Nipype.
 
 Usage:
 
-$(basename $0) [-h] [-b] [-m]
+$(basename "$0") [-h] [-b] [-m]
 
 Options:
 
diff --git a/docker/prune_dockerfile.sh b/docker/prune_dockerfile.sh
index e6b05ebbcf..fd47fd6e9e 100644
--- a/docker/prune_dockerfile.sh
+++ b/docker/prune_dockerfile.sh
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 if [ -z "$1" ]; then
-  echo "Usage: $(basename $0) <input_filepath>"
+  echo "Usage: $(basename "$0") <input_filepath>"
   exit 1
 fi
 
diff --git a/tools/ci/check.sh b/tools/ci/check.sh
index 5151854902..22486840a8 100755
--- a/tools/ci/check.sh
+++ b/tools/ci/check.sh
@@ -8,7 +8,7 @@ source tools/ci/env.sh
 set -eu
 
 # Required variables
-echo CHECK_TYPE = $CHECK_TYPE
+echo CHECK_TYPE = "$CHECK_TYPE"
 
 set -x
 
diff --git a/tools/ci/create_venv.sh b/tools/ci/create_venv.sh
index 7a28767396..8c6db79cfa 100755
--- a/tools/ci/create_venv.sh
+++ b/tools/ci/create_venv.sh
@@ -7,7 +7,7 @@ source tools/ci/env.sh
 set -eu
 
 # Required variables
-echo SETUP_REQUIRES = $SETUP_REQUIRES
+echo SETUP_REQUIRES = "$SETUP_REQUIRES"
 
 set -x
 
@@ -15,7 +15,7 @@ python -m pip install --upgrade pip virtualenv
 virtualenv --python=python virtenv
 source tools/ci/activate.sh
 python --version
-python -m pip install -U $SETUP_REQUIRES
+python -m pip install -U "$SETUP_REQUIRES"
 which python
 which pip
 
diff --git a/tools/ci/install.sh b/tools/ci/install.sh
index 428ffc8b8c..05e8731ec6 100755
--- a/tools/ci/install.sh
+++ b/tools/ci/install.sh
@@ -8,10 +8,10 @@ source tools/ci/env.sh
 set -eu
 
 # Required variables
-echo INSTALL_TYPE = $INSTALL_TYPE
-echo CHECK_TYPE = $CHECK_TYPE
-echo NIPYPE_EXTRAS = $NIPYPE_EXTRAS
-echo EXTRA_PIP_FLAGS = $EXTRA_PIP_FLAGS
+echo INSTALL_TYPE = "$INSTALL_TYPE"
+echo CHECK_TYPE = "$CHECK_TYPE"
+echo NIPYPE_EXTRAS = "$NIPYPE_EXTRAS"
+echo EXTRA_PIP_FLAGS = "$EXTRA_PIP_FLAGS"
 
 set -x
 
@@ -22,7 +22,7 @@ fi
 if [ "$INSTALL_TYPE" == "setup" ]; then
     python setup.py install
 else
-    pip install $EXTRA_PIP_FLAGS $ARCHIVE
+    pip install "$EXTRA_PIP_FLAGS" "$ARCHIVE"
 fi
 
 # Basic import check
@@ -32,7 +32,7 @@ if [ "$CHECK_TYPE" == "skiptests" ]; then
     exit 0
 fi
 
-pip install $EXTRA_PIP_FLAGS "nipype[$NIPYPE_EXTRAS]"
+pip install "$EXTRA_PIP_FLAGS" "nipype[$NIPYPE_EXTRAS]"
 
 set +eux
 
diff --git a/tools/ci/install_dependencies.sh b/tools/ci/install_dependencies.sh
index 617389cb5e..b679b8f215 100755
--- a/tools/ci/install_dependencies.sh
+++ b/tools/ci/install_dependencies.sh
@@ -8,8 +8,8 @@ source tools/ci/env.sh
 set -eu
 
 # Required variables
-echo EXTRA_PIP_FLAGS = $EXTRA_PIP_FLAGS
-echo DEPENDS = $DEPENDS
+echo EXTRA_PIP_FLAGS = "$EXTRA_PIP_FLAGS"
+echo DEPENDS = "$DEPENDS"
 
 set -x
 
@@ -18,7 +18,7 @@ if [ -n "$EXTRA_PIP_FLAGS" ]; then
 fi
 
 if [ -n "$DEPENDS" ]; then
-    pip install ${EXTRA_PIP_FLAGS} --prefer-binary ${!DEPENDS}
+    pip install "${EXTRA_PIP_FLAGS}" --prefer-binary "${!DEPENDS}"
 fi
 
 set +eux
diff --git a/tools/feedstock.sh b/tools/feedstock.sh
index 831f04cf39..5fdb7b9232 100755
--- a/tools/feedstock.sh
+++ b/tools/feedstock.sh
@@ -51,22 +51,22 @@ else
 fi
 
 # Clean working copy
-TMP=`mktemp -d`
-hub clone conda-forge/$FEEDSTOCK $TMP/$FEEDSTOCK
-pushd $TMP/$FEEDSTOCK
+TMP=$(mktemp -d)
+hub clone conda-forge/"$FEEDSTOCK" "$TMP"/"$FEEDSTOCK"
+pushd "$TMP"/"$FEEDSTOCK"
 
 # Get user fork, move to a candidate release branch, detecting if new branch
 hub fork
 git fetch --all
-if git checkout -t $GITHUB_USER/$BRANCH; then
+if git checkout -t "$GITHUB_USER"/"$BRANCH"; then
     NEW_PR=false
 else
     NEW_PR=true
-    git checkout -b $BRANCH origin/main
+    git checkout -b "$BRANCH" origin/main
 fi
 
 # Calculate hash
-SHA256=`curl -sSL https://github.com/$SRCREPO/archive/$REF.tar.gz | sha256sum | cut -d\  -f 1`
+SHA256=$(curl -sSL https://github.com/"$SRCREPO"/archive/"$REF".tar.gz | sha256sum | cut -d\  -f 1)
 
 URL_BASE="https://github.com/$CIRCLE_PROJECT_USERNAME/{{ name }}/archive"
 if $RELEASE; then
@@ -87,7 +87,7 @@ sed -i \
 # Bump branch
 git add recipe/meta.yaml
 git commit -m "$COMMIT_MSG"
-git push -u $GITHUB_USER $BRANCH
+git push -u "$GITHUB_USER" "$BRANCH"
 
 if $NEW_PR; then
     hub pull-request -b conda-forge:main -F - <<END
@@ -117,4 +117,4 @@ fi
 
 # Remove working copy
 popd
-rm -rf $TMP/$FEEDSTOCK
+rm -rf "$TMP"/"$FEEDSTOCK"
diff --git a/tools/retry_cmd.sh b/tools/retry_cmd.sh
index 78e9c40924..879b9ce08a 100755
--- a/tools/retry_cmd.sh
+++ b/tools/retry_cmd.sh
@@ -26,11 +26,11 @@ for ARG; do
 done
 
 RET=0
-for i in `seq $NLOOPS`; do
+for i in $(seq "$NLOOPS"); do
     sh -c "$CMD"
     RET="$?"
     if [ "$RET" -eq 0 ]; then break; fi
-    if [ "$i" -ne "$NLOOPS" ]; then sleep $TOSLEEP; fi
+    if [ "$i" -ne "$NLOOPS" ]; then sleep "$TOSLEEP"; fi
 done
 
 exit $RET
diff --git a/tools/update_changes.sh b/tools/update_changes.sh
index 5f7afc6057..7c4117dabd 100755
--- a/tools/update_changes.sh
+++ b/tools/update_changes.sh
@@ -16,12 +16,12 @@ ROOT=$( git rev-parse --show-toplevel )
 CHANGES=$ROOT/doc/changelog/1.X.X-changelog.rst
 
 # Check whether the Upcoming release header is present
-head -1 $CHANGES | grep -q Upcoming
+head -1 "$CHANGES" | grep -q Upcoming
 UPCOMING=$?
 
 # Elaborate today's release header
 HEADER="$1 ($(date '+%B %d, %Y'))"
-echo $HEADER >> newchanges
+echo "$HEADER" >> newchanges
 echo $( printf "%${#HEADER}s" | tr " " "=" ) >> newchanges
 echo >> newchanges
 
@@ -32,17 +32,17 @@ if [[ "x$MILESTONE" != "x" ]]; then
 fi
 
 # Search for PRs since previous release
-MERGE_COMMITS=$( git log --grep="Merge pull request\|(#.*)$" `git describe --tags --abbrev=0`..HEAD --pretty='format:%h' )
+MERGE_COMMITS=$( git log --grep="Merge pull request\|(#.*)$" $(git describe --tags --abbrev=0)..HEAD --pretty='format:%h' )
 for COMMIT in ${MERGE_COMMITS//\n}; do
-    SUB=$( git log -n 1 --pretty="format:%s" $COMMIT )
-    if ( echo $SUB | grep "^Merge pull request" ); then
+    SUB=$( git log -n 1 --pretty="format:%s" "$COMMIT" )
+    if ( echo "$SUB" | grep "^Merge pull request" ); then
         # Merge commit
-        PR=$( echo $SUB | sed -e "s/Merge pull request \#\([0-9]*\).*/\1/" )
-        TITLE=$( git log -n 1 --pretty="format:%b" $COMMIT )
+        PR=$( echo "$SUB" | sed -e "s/Merge pull request \#\([0-9]*\).*/\1/" )
+        TITLE=$( git log -n 1 --pretty="format:%b" "$COMMIT" )
     else
         # Squashed merge
-        PR=$( echo $SUB | sed -e "s/.*(\#\([0-9]*\))$/\1/" )
-        TITLE=$( echo $SUB | sed -e "s/\(.*\)(\#[0-9]*)$/\1/" )
+        PR=$( echo "$SUB" | sed -e "s/.*(\#\([0-9]*\))$/\1/" )
+        TITLE=$( echo "$SUB" | sed -e "s/\(.*\)(\#[0-9]*)$/\1/" )
     fi
     echo "  * $TITLE (https://github.com/nipy/nipype/pull/$PR)" >> newchanges
 done
@@ -52,10 +52,10 @@ echo >> newchanges
 # Append old CHANGES
 if [[ "$UPCOMING" == "0" ]]; then
     # Drop the Upcoming title if present
-    tail -n+4 $CHANGES >> newchanges
+    tail -n+4 "$CHANGES" >> newchanges
 else
-    cat $CHANGES >> newchanges
+    cat "$CHANGES" >> newchanges
 fi
 
 # Replace old CHANGES with new file
-mv newchanges $CHANGES
+mv newchanges "$CHANGES"
diff --git a/tools/update_mailmap.sh b/tools/update_mailmap.sh
index 4602e85e3a..32d0786f00 100644
--- a/tools/update_mailmap.sh
+++ b/tools/update_mailmap.sh
@@ -8,15 +8,15 @@ set -ux
 ROOT=$( git rev-parse --show-toplevel )
 MAILMAP=$ROOT/.mailmap
 
-LAST=$(git describe --tags `git rev-list --tags --max-count=1`)
+LAST=$(git describe --tags $(git rev-list --tags --max-count=1))
 RELEASE=${1:-$LAST}
 
 IFS=$'\n'
-for NAME in $(git shortlog -nse $RELEASE.. | cut -f2-); do
-    echo $NAME
+for NAME in $(git shortlog -nse "$RELEASE".. | cut -f2-); do
+    echo "$NAME"
 done
 
 # sort and write
-sort $MAILMAP > .tmpmailmap
-cp .tmpmailmap $MAILMAP
+sort "$MAILMAP" > .tmpmailmap
+cp .tmpmailmap "$MAILMAP"
 rm .tmpmailmap