diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ca918594d2f9e9a23804f00b7249b2aeb3cf5491..81ad38ce796f0e9e24a441766d0472ceb49580d9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -66,7 +66,7 @@ build-debug:
     - cmake --version
     - ccache --version
     - ninja --version
-    - ./sbs --debug
+    - ./sbs build --debug
 
 build-release:
   stage: build
@@ -74,7 +74,7 @@ build-release:
     - cmake --version
     - ccache --version
     - ninja --version
-    - ./sbs
+    - ./sbs build
 
 logdecoder:
   stage: build
@@ -88,7 +88,7 @@ logdecoder:
 test:
   stage: test
   script:
-    - ./sbs --test catch-tests-boardcore
+    - ./sbs test
 
 # Stage documentation
 
diff --git a/sbs b/sbs
index bdf753882f2a1461e070f5e59d8ce3251052b6e8..83b598b42e6b4797d2891dcd8d13c8073e144257 100755
--- a/sbs
+++ b/sbs
@@ -28,9 +28,10 @@
 # Terminal colors
 TTY_RESET="\033[0m"
 TTY_BOLD="\033[1m"
-TTY_FOUND="\033[38;5;200m"
-TTY_SUCCESS="\033[38;5;40m"
-TTY_ERROR="\033[38;5;160m"
+TTY_FOUND="\033[35m"
+TTY_SUCCESS="\033[32m"
+TTY_ERROR="\033[31m"
+TTY_STEP="\033[94m"
 
 TTY_LOGO_1="\033[38;5;200m"
 TTY_LOGO_2="\033[38;5;164m"
@@ -41,6 +42,14 @@ TTY_LOGO_5="\033[38;5;27m"
 # Error message
 ERR="\n"$TTY_ERROR""$TTY_BOLD"ERROR"$TTY_RESET""
 
+# Error codes
+EPERM=1   # Operation not permitted
+ENOENT=2  # No such file or directory
+ENOEXEC=8 # Exec format error
+ENODEV=19 # No such device
+EINVAL=22 # Invalid argument
+ENOPKG=65 # Package not installed
+
 # Filenames/Dirnames
 CMAKE_FILENAME="CMakeCache.txt"
 CTEST_FILENAME="CTestTestfile.cmake"
@@ -107,8 +116,8 @@ print_configuration() {
 		printf "$nf\n"
 	fi
 
-	[ "$found_cmake" = true ]     || { printf ""$ERR": CMake must be installed\n"; return 1; }
-    [ "$found_miosixgpp" = true ] || { printf ""$ERR": arm-miosix-eabi-g++ must be installed\n"; return 1; }
+	[ "$found_cmake" = true ]     || { printf ""$ERR": CMake must be installed\n"; return $ENOPKG; }
+    [ "$found_miosixgpp" = true ] || { printf ""$ERR": arm-miosix-eabi-g++ must be installed\n"; return $ENOPKG; }
 }
 
 ################################################################################
@@ -160,7 +169,7 @@ filter_args() {
 
 # Checks that no unknown flags are passed
 # The syntax is: cmd_flags "known_flags" "flags"
-cmd_flags() {    
+cmd_flags() {
     known_flags=("${!1}")
     flags=("${!2}")
 
@@ -188,7 +197,7 @@ cmd_flags() {
 
         if [ "$found" = false ]; then
             printf ""$ERR": Unknown flag $flag\n"
-            return 1
+            return $EINVAL
         fi
     done
 }
@@ -201,7 +210,7 @@ cmd_args() {
     args=("${!1}")
     if [ "${#args[@]}" -gt "$max_args" ]; then
         printf ""$ERR": Too many arguments\n"
-        return 1
+        return $EINVAL
     fi
 }
 
@@ -238,7 +247,7 @@ get_flag_value() {
 
 # Print a step message
 step() {
-	printf "\n"$TTY_LOGO_5""$TTY_BOLD"$1"$TTY_RESET"\n"
+	printf "\n"$TTY_STEP""$TTY_BOLD"$1"$TTY_RESET"\n"
 	# Print - for every letter in the pre string
 	for (( i=0; i<${#1}; i++ )); do printf -- "-"; done;
 	printf "\n";
@@ -327,7 +336,7 @@ check_configured() {
         printf "  - Verbose: %s\n" "$config_verbose"
         printf "  - Host:    %s\n" "$config_host"
 
-        [ -f "$toolchain_file" ] || { printf ""$ERR": CMake Toolchain File for Miosix was not found\n"; return 1; }
+        [ -f "$toolchain_file" ] || { printf ""$ERR": CMake Toolchain File for Miosix was not found\n"; return $ENOPKG1; }
         
         declare -a defs=(-DCMAKE_EXPORT_COMPILE_COMMANDS=ON)
         defs+=(-DCMAKE_C_FLAGS=-fdiagnostics-color=always -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always)
@@ -346,6 +355,47 @@ check_configured() {
     fi
 }
 
+build_impl() {
+    target="$1"
+    build_dir="$2"
+    config_debug="$3"
+    config_verbose="$4"
+    config_host="$5"
+
+    check_configured "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
+    
+    step "Build"
+
+    declare opts
+    get_build_opts opts $jobs
+
+    cmake --build "$build_dir" "${opts[@]}" --target "$target"
+}
+
+flash_impl() {
+    target="$1"
+    build_dir="$2"
+    reset="$3"
+
+    # check if the target is flashable
+    [ -f "$build_dir/$target.bin" ] || { printf ""$ERR": target '$target' is not flashable"; return $ENOEXEC; }
+    
+    # flash the target
+    step "Flash"
+
+    declare -a flash_opts
+    [ "$reset" = true ] && flash_opts+=("--connect-under-reset")
+
+    if [ "$found_stflash" = true ]; then
+        st-flash --reset "${flash_opts[@]}" write "$build_dir/$target.bin" 0x8000000 
+    elif [ "$found_stlink" = true ]; then
+        ST-LINK_CLI.exe -P "$build_dir/$target.bin" 0x8000000 -V -Rst
+    else
+        printf ""$ERR": No flashing software found!\n"
+        return $ENOPKG
+    fi
+}
+
 lint_copyright() {
     step "Lint copyright"
 
@@ -428,14 +478,7 @@ build() {
 
     target=${args[0]:-all}
 
-    check_configured "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
-    
-    step "Build"
-
-    declare opts
-    get_build_opts opts $jobs
-    
-    cmake --build "$build_dir" "${opts[@]}" --target "$target"
+    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host"
 }
 
 clean() {
@@ -476,36 +519,76 @@ flash() {
 
     if [ -z "$target" ]; then
         printf ""$ERR": No target specified\n"
-        return 1
+        return $ENOENT
     fi
 
-    check_configured "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
-    
     # build the target
-    step "Build"
-
-    declare opts
-    get_build_opts opts $jobs
-    
-    cmake --build "$build_dir" "${opts[@]}" --target "$target"
-    
-    # check if the target is flashable
-    [ -f "$build_dir/$target.bin" ] || { printf ""$ERR": target '$target' is not flashable"; return 1; }
+    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
     
     # flash the target
-    step "Flash"
+    flash_impl "$target" "$build_dir" "$reset"
+}
 
-    flash_opts=()
-    [ "$reset" = true ] && flash_opts+=("--connect-under-reset")
+run() {
+    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
+    IFS=' ' read -r -a args <<< "$(filter_args "$@")"
 
-    if [ "$found_stflash" = true ]; then
-        st-flash --reset "${flash_opts[@]}" write "$build_dir/$target.bin" 0x8000000 
-    elif [ "$found_stlink" = true ]; then
-        ST-LINK_CLI.exe -P "$build_dir/$target.bin" 0x8000000 -V -Rst
-    else
-        printf ""$ERR": No flashing software found!\n"
-        return 1
+    jobs_flag=("-j" "--jobs")
+    device_flag=("-D" "--device")
+    baudrate_flag=("-B" "--baudrate")
+    known_flags=("-d" "-v" "-r" "-D=" "-B=" "-j=" "--debug" "--verbose" "--reset" "--device=" "--baudrate=" "--jobs=")
+
+    cmd_flags known_flags[@] flags[@] || return
+    cmd_args 1 args[@] || return
+
+    build_dir="$build_default_dir"
+
+    jobs=$(get_flag_value jobs_flag[@] flags[@])
+
+    [ "$(flag_exists "-d" flags[@])" = true ] || [ "$(flag_exists "--debug"   flags[@])" = true ] && config_debug=true   || config_debug=false
+    [ "$(flag_exists "-v" flags[@])" = true ] || [ "$(flag_exists "--verbose" flags[@])" = true ] && config_verbose=true || config_verbose=false
+    [ "$(flag_exists "-r" flags[@])" = true ] || [ "$(flag_exists "--reset"   flags[@])" = true ] && reset=true          || reset=false
+    config_host=false
+
+    target=${args[0]}
+
+    if [ -z "$target" ]; then
+        printf ""$ERR": No target specified\n"
+        return $ENOENT
+    fi
+
+    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
+
+    flash_impl "$target" "$build_dir" "$reset" || return
+
+    device=$(get_flag_value device_flag[@] flags[@])
+    baudrate=$(get_flag_value baudrate_flag[@] flags[@])
+
+    # If no device was specified, try to find it based on what we commonly use
+    if [ -z "$device" ]; then
+        if [ -e "/dev/ttyACM0" ]; then
+            device="/dev/ttyACM0"
+        elif [ -e "/dev/ttyUSB0" ]; then
+            device="/dev/ttyUSB0"
+        else
+            printf ""$ERR": No device specified and no default device found\n"
+            return $ENODEV
+        fi
     fi
+
+    # Set Skyward's default baudrate, if none was specified
+    if [ -z "$baudrate" ]; then
+        baudrate="115200"
+    fi
+
+    step "Run - $device @ $baudrate"
+
+    # tty devices use \r\n for newlines by default
+    # disable newlines on \r with `-icrnl` to avoid double newlines
+    stty --file $device $baudrate -icrnl || return
+    
+    # connect to the device
+    cat $device
 }
 
 test() {
@@ -513,35 +596,29 @@ test() {
     IFS=' ' read -r -a args <<< "$(filter_args "$@")"
 
     jobs_flag=("-j" "--jobs")
-    known_flags=("-j=" "--jobs=")
+    known_flags=("-d" "-v" "-j=" "--debug" "--verbose" "--jobs=")
 
     cmd_flags know_flags[@] flags[@] || return
     cmd_args 1 args[@] || return
 
-    config_debug=true
-    config_verbose=false
-    config_host=true
     build_dir="$build_host_dir"
 
     jobs=$(get_flag_value jobs_flag[@] flags[@])
 
-    target=${args[0]}
+    [ "$(flag_exists "-d" flags[@])" = true ] || [ "$(flag_exists "--debug"   flags[@])" = true ] && config_debug=true   || config_debug=false
+    [ "$(flag_exists "-v" flags[@])" = true ] || [ "$(flag_exists "--verbose" flags[@])" = true ] && config_verbose=true || config_verbose=false
+    config_host=true
+
+    target=${args[0]:-catch-tests-boardcore}
 
     if [ -z "$target" ]; then
         printf ""$ERR": No target specified\n"
-        return 1
+        return $ENOENT
     fi
 
-    check_configured "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
-
     # build the target
-    step "Build"
+    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return
 
-    declare opts
-    get_build_opts opts $jobs
-    
-    cmake --build "$build_dir" "${opts[@]}" --target "$target"
-    
     # run the tests
     step "Test"
     
@@ -569,7 +646,7 @@ list() {
 
     if [ -z "$list_type" ]; then
         printf ""$ERR": No list type specified\n"
-        return 1
+        return $EINVAL
     fi
 
     if [ "$list_type" = "targets" ]; then
@@ -579,7 +656,7 @@ list() {
         cmake --build "$build_dir" --target help | awk -F '[-:]' '/^boardcore/ {print $2}'
     else
         printf ""$ERR": Unknown list type $list_type\n"
-        return 1
+        return $EINVAL
     fi
 }
 
@@ -643,12 +720,15 @@ help() {
     echo "Usage: sbs (command) <args> [options] "
     echo ""
     echo "Commands:"
-    echo " build     - Build the specified target"
+    echo " build     - Build the specified target. If none is specified, all targets are built"
     echo "           -> <target=all> [-d | --debug] [-v | --verbose] [-j | --jobs=<jobs>]"
-    echo " flash     - Flash the specified target"
+    echo " flash     - Build and flash the specified target"
+    echo "           -> <target> [-d | --debug] [-v | --verbose] [-r | --reset] [-j | --jobs=<jobs>]"
+    echo " run       - Build and flash the specified target and connect to serial device"
     echo "           -> <target> [-d | --debug] [-v | --verbose] [-r | --reset] [-j | --jobs=<jobs>]"
-    echo " test      - Run the tests for the specified target"
-    echo "           -> <target> [-j | --jobs=<jobs>]"
+    echo "                       [-D | --device=<device>] [-B | --baudrate=<baudrate>]"
+    echo " test      - Run the specified test. If none is specified, catch tests are run"
+    echo "           -> <target> [-d | --debug] [-v | --verbose] [-j | --jobs=<jobs>]"
     echo " list      - List the available targets or boards"
     echo "           -> <targets | boards>"
     echo " clean     - Clean the build directory"
@@ -658,10 +738,12 @@ help() {
     echo " uninstall - Uninstall autocomplete"
     echo ""
     echo "Options:"
-    echo " -d, --debug:   Build in debug mode"
-    echo " -v, --verbose: Build in verbose mode"
-    echo " -r, --reset:   Reset the target before flashing"
-    echo " -j, --jobs:    Number of jobs to run in parallel"
+    echo " -d, --debug:    Build in debug mode"
+    echo " -v, --verbose:  Build in verbose mode"
+    echo " -r, --reset:    Reset the target before flashing"
+    echo " -j, --jobs:     Number of jobs to run in parallel"
+    echo " -D, --device:   Serial device to connect to (default: /dev/ttyACM0 or /dev/ttyUSB0)"
+    echo " -B, --baudrate: Baudrate for the serial device (default: 115200)"
     echo ""
 }
 
@@ -683,7 +765,7 @@ install() {
 
     if [ "$found_python" = false ]; then
         printf ""$ERR": Python is required to install autocomplete\n"
-        return 1
+        return $ENOPKG
     fi
 
     echo "Retrieving targets..."
@@ -711,7 +793,7 @@ uninstall() {
 
     if [ "$found_python" = false ]; then
         printf ""$ERR": Python is required to uninstall autocomplete\n"
-        return 1
+        return $ENOPKG
     fi
 
     python "$sbs_base/scripts/autocomplete.py" "--uninstall"
@@ -740,16 +822,18 @@ for arg in "$@"; do
         build)     init; build "${@:2}"; exit ;;
         clean)     init; clean "${@:2}"; exit ;;
         flash)     init; flash "${@:2}"; exit ;;
+        run)       init; run   "${@:2}"; exit ;;
         test)      init; test  "${@:2}"; exit ;;
         lint)      init; lint  "${@:2}"; exit ;;
         format)    init; format; exit ;;
         install)   init; install; exit ;;
         uninstall) init; uninstall; exit ;;
         list)      init_no_output; list  "${@:2}"; exit ;;
-        *)         help; exit ;;
+        *)         help; exit $EINVAL;;
     esac
 done
 
 if [ "$#" -eq 0 ]; then
     help
-fi
\ No newline at end of file
+    exit $EINVAL
+fi
diff --git a/scripts/autocomplete.py b/scripts/autocomplete.py
index d90181a01fd4ab7344cd68e3578c5c83e9fa037f..539c6ef23807045adc0a8cd7adc74103fecaad8e 100755
--- a/scripts/autocomplete.py
+++ b/scripts/autocomplete.py
@@ -33,6 +33,7 @@ COMMANDS = [
     "build",
     "clean",
     "flash",
+    "run",
     "list",
     "test",
     "lint",
@@ -40,7 +41,7 @@ COMMANDS = [
     "install",
     "uninstall",
 ]
-COMMANDS_WITH_TARGET = ["build", "flash", "test"]
+COMMANDS_WITH_TARGET = ["build", "flash", "run", "test"]
 
 
 def strip_extension(file: str):