diff --git a/.github/.cspell/project-dictionary.txt b/.github/.cspell/project-dictionary.txt
index f82c643c..cf792e66 100644
--- a/.github/.cspell/project-dictionary.txt
+++ b/.github/.cspell/project-dictionary.txt
@@ -6,10 +6,10 @@ nanos
 nanosec
 nsec
 nsecs
+openrr
 rosmsg
 rustc
 rustdocflags
 rustflags
 rustros
-rustup
 slerp
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..cc5b96fe
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,19 @@
+version: 2
+updates:
+  - package-ecosystem: cargo
+    directory: /
+    schedule:
+      interval: daily
+    commit-message:
+      prefix: ''
+    ignore:
+      # These dependencies need to be updated at the same time with openrr/openrr.
+      - dependency-name: nalgebra
+    labels: []
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: daily
+    commit-message:
+      prefix: ''
+    labels: []
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 21aa22fd..fb5ae94f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,7 +49,7 @@ jobs:
       - name: Install ROS2
         run: |
           # for tf_r2r
-          sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key  -o /usr/share/keyrings/ros-archive-keyring.gpg
+          sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
           echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
           sudo apt-get update
           sudo apt-get install -y ros-$ROS2_DISTRO-ros-core ros-$ROS2_DISTRO-geometry-msgs ros-$ROS2_DISTRO-tf2-msgs
@@ -83,7 +83,7 @@ jobs:
       - name: Install ROS2
         run: |
           # for tf_r2r
-          sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key  -o /usr/share/keyrings/ros-archive-keyring.gpg
+          sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
           echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
           sudo apt-get update
           sudo apt-get install -y ros-$ROS2_DISTRO-ros-core ros-$ROS2_DISTRO-geometry-msgs ros-$ROS2_DISTRO-tf2-msgs
@@ -96,6 +96,34 @@ jobs:
   spell-check:
     runs-on: ubuntu-latest
     timeout-minutes: 60
+    permissions:
+      contents: write
+      pull-requests: write
     steps:
       - uses: actions/checkout@v4
+      - run: echo "REMOVE_UNUSED_WORDS=1" >>"${GITHUB_ENV}"
+        if: github.repository_owner == 'openrr' && (github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main')
       - run: tools/spell-check.sh
+      - id: diff
+        run: |
+          set -euo pipefail
+          git config user.name "Taiki Endo"
+          git config user.email "taiki@smilerobotics.com"
+          git add -N .github/.cspell
+          if ! git diff --exit-code -- .github/.cspell; then
+            git add .github/.cspell
+            git commit -m "Update cspell dictionary"
+            echo 'success=false' >>"${GITHUB_OUTPUT}"
+          fi
+        if: github.repository_owner == 'openrr' && (github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main')
+      - uses: peter-evans/create-pull-request@v7
+        with:
+          title: Update cspell dictionary
+          body: |
+            Auto-generated by [create-pull-request][1]
+            [Please close and immediately reopen this pull request to run CI.][2]
+
+            [1]: https://github.com/peter-evans/create-pull-request
+            [2]: https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#workarounds-to-trigger-further-workflow-runs
+          branch: update-cspell-dictionary
+        if: github.repository_owner == 'openrr' && (github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main') && steps.diff.outputs.success == 'false'
diff --git a/tools/spell-check.sh b/tools/spell-check.sh
index 89ef6ad1..17e9a023 100755
--- a/tools/spell-check.sh
+++ b/tools/spell-check.sh
@@ -26,6 +26,13 @@ error() {
     fi
     should_fail=1
 }
+warn() {
+    if [[ -n "${GITHUB_ACTIONS:-}" ]]; then
+        echo "::warning::$*"
+    else
+        echo >&2 "warning: $*"
+    fi
+}
 
 project_dictionary=.github/.cspell/project-dictionary.txt
 has_rust=''
@@ -61,11 +68,17 @@ EOF
 if [[ -n "${dependencies_words:-}" ]]; then
     echo $'\n'"${dependencies_words}" >>.github/.cspell/rust-dependencies.txt
 fi
-check_diff .github/.cspell/rust-dependencies.txt
+if [[ -z "${REMOVE_UNUSED_WORDS:-}" ]]; then
+    check_diff .github/.cspell/rust-dependencies.txt
+fi
 
 echo "+ npx -y cspell --no-progress --no-summary \$(git ls-files)"
 if ! npx -y cspell --no-progress --no-summary $(git ls-files); then
-    error "spellcheck failed: please fix uses of above words or add to ${project_dictionary} if correct"
+    error "spellcheck failed: please fix uses of below words or add to ${project_dictionary} if correct"
+    echo >&2 "======================================="
+    (npx -y cspell --no-progress --no-summary --words-only $(git ls-files) || true) | LC_ALL=C sort -f -u >&2
+    echo >&2 "======================================="
+    echo >&2
 fi
 
 # Make sure the project-specific dictionary does not contain duplicated words.
@@ -76,24 +89,41 @@ for dictionary in .github/.cspell/*.txt; do
     dup=$(sed '/^$/d' "${project_dictionary}" "${dictionary}" | LC_ALL=C sort -f | uniq -d -i | (grep -v '//.*' || true))
     if [[ -n "${dup}" ]]; then
         error "duplicated words in dictionaries; please remove the following words from ${project_dictionary}"
-        echo "======================================="
-        echo "${dup}"
-        echo "======================================="
+        echo >&2 "======================================="
+        echo >&2 "${dup}"
+        echo >&2 "======================================="
+        echo >&2
     fi
 done
 
 # Make sure the project-specific dictionary does not contain unused words.
-unused=''
-for word in $(grep -v '//.*' "${project_dictionary}" || true); do
-    if ! grep <<<"${all_words}" -Eq -i "^${word}$"; then
-        unused+="${word}"$'\n'
+if [[ -n "${REMOVE_UNUSED_WORDS:-}" ]]; then
+    grep_args=()
+    for word in $(grep -v '//.*' "${project_dictionary}" || true); do
+        if ! grep <<<"${all_words}" -Eq -i "^${word}$"; then
+            # TODO: single pattern with ERE: ^(word1|word2..)$
+            grep_args+=(-e "^${word}$")
+        fi
+    done
+    if [[ ${#grep_args[@]} -gt 0 ]]; then
+        warn "removing unused words from ${project_dictionary}"
+        res=$(grep -v "${grep_args[@]}" "${project_dictionary}")
+        echo "${res}" >"${project_dictionary}"
+    fi
+else
+    unused=''
+    for word in $(grep -v '//.*' "${project_dictionary}" || true); do
+        if ! grep <<<"${all_words}" -Eq -i "^${word}$"; then
+            unused+="${word}"$'\n'
+        fi
+    done
+    if [[ -n "${unused}" ]]; then
+        warn "unused words in dictionaries; please remove the following words from ${project_dictionary}"
+        echo >&2 "======================================="
+        echo >&2 -n "${unused}"
+        echo >&2 "======================================="
+        echo >&2
     fi
-done
-if [[ -n "${unused}" ]]; then
-    error "unused words in dictionaries; please remove the following words from ${project_dictionary}"
-    echo "======================================="
-    echo -n "${unused}"
-    echo "======================================="
 fi
 
 if [[ -n "${should_fail:-}" ]]; then