There is lots of small stuff that muddies seeing the overall flow.

My apologizes for only a primarily low-level review.

1. Drop though cases.  The code _appears_ to be OK. Yet it looks like it might be missing a `break;` at the end of `case '?'`.  Recommend to add a comment at the end of a case that intentionally lacks a `break`, `return`,etc.

            case '?':
                if (optopt == 'c')
                    fprintf(stderr, "Option -%c requires an argument.\n", optopt);
                else if (isprint (optopt))
                    fprintf(stderr, "Unknown option `-%c'.\n", optopt);
                else
                    fprintf(stderr,
                            "Unknown option character `\\x%x'.\n",
                            optopt);

                // Add comment
                // Drop though
               
            default: {
                return 1;
            }

2. Remove debug code

                printf("sh OpenShell version 0.1(a)\n");
                printf("Version: %s\n", VERSION);

              //  printf ("%s / %s / %s / %s\n",
                //         program_name, version,
                  //       build_date, build_git_sha);

3. Obviously missing include files and other code needed before `main()`.

        int main(int argc, char *argv[]) {
            bool donotrun = false;
            struct sigaction new_action, old_action;
            hashtable_t *hashtable = ht_create(65536);

4. Inconsistent indentation. An `if()` without `{}` is tolerable, yet not preferred, yet then code breaks a line that would look fine as one.  Suggest re-formating.

        // if (old_action.sa_handler != SIG_IGN)
        //     sigaction(SIGINT, &new_action, NULL);
        // ...
        // i = getopt_long(argc, argv, "pc:fvh",
        //                 options, &index);

        if (old_action.sa_handler != SIG_IGN) {
            sigaction(SIGINT, &new_action, NULL);
        }
        ...
        i = getopt_long(argc, argv, "pc:fvh", options, &index);

5.  Inconsistent formating in `switch statement concerning blank lines.  This and other parts hints that OP is not using an automated formatter.  Save time.  Use an automated formatter.  Often these are incorporated within a design environment.  Stand alone ones exist.  Avoid manual formatting.

                exit(EXIT_SUCCESS);

            }
            case 'h': {
                usage();
                exit(EXIT_SUCCESS);

            }
            case 'c': {
                cvalue = optarg;
                command(cvalue, hashtable, background);
                exit(EXIT_SUCCESS);
            }

            case 'f': {

6. `getPath();` is undefined, naked and uncommented.  Certainly such a call could return an error, one that is not handled here.

7. Buffer over flow potential.  There is nothing special about 128.  Code needs protection/re-work.

                char str[128];
                strcpy(str, line);  // Unsafe

8. For array indexing use `size_t`.  

                // int cc;
                size_t cc;

                cc = strlen(buf);

9. Avoid a hacker exploit.  `buf[0]` could be 0 and then `buf[cc - 1]` is UB.  Instead test.

        if (fgets(buf, CMD_LEN - 1, fp) == NULL) {
        ...
        cc = strlen(buf);
        //if (buf[cc - 1] == '\n')
        if (cc > 0 && buf[cc - 1] == '\n')

10.  Simplification

        //char buffer[2];
        //buffer[0] = '|';
        //buffer[1] = '\0';
        char buffer[] = "|";

11. Avoid allocation by size of type.  Instead by the size of the referenced variable.  This post does not show `struct pipeline`.  OTOH, with the right code, we can have less need for it.  BTW, pedantic point: good to do allocation math leading with `sizeof` as that insures math at least at `size_t` width.  Consider `malloc(BUFFER_SIZE * chunks[i].size * sizeof *(pipe[i].data))` _could_ overflow the `BUFFER_SIZE * chunks[i].size` first done in that order.

         // pipe[i].option = malloc(sizeof(int) * 10);
         pipe[i].option = malloc(sizeof *(pipe[i].option) * 10);

         // pipe[i].data = malloc(sizeof(char **) * BUFFER_SIZE * chunks[i].size);
         pipe[i].data = malloc(sizeof *(pipe[i].data) * BUFFER_SIZE * chunks[i].size);

12. Allocation test good, but needs an exit.  Good to test, but then what?  Suggest returning a failure code too.  IMO, better to include some diagnostic info like `__LINE__`, `__FUNC__`, etc.

        if (pipe == NULL) {
            // fprintf(stderr, "malloc failed!\n");
           fprintf(stderr, "malloc failed! %d\n", __LINE__);
            // add
            return(EXIT_FAILURE);
        }

11. Some systems have a multitude of special integer types.  Rather than guess at their width and potentially truncate, just go for the largest supported on printing

         // fprintf(stderr, "pid %ld:\n", (long) pid);
         fprintf(stderr, "pid %lld:\n", (long long) pid);
         // or
         fprintf(stderr, "pid %jd:\n", (intmax_t) pid);

12. Unclear if the result of `list_split()` is _always_ `chunks->pipes > 0`.  If it could be 0, then `malloc(0)` returning `NULL` is _not_ a sign of OOM.

        struct str_list *chunks = list_split(cmd, buffer);
        struct pipeline *pipe = malloc(chunks->pipes * sizeof *pipe);
        // if (pipe == NULL) {
        if (pipe == NULL && chunks->pipes > 0) {
            fprintf(stderr, "malloc failed!\n");
        }

15.  Code is certainly wrong - passing an unknown value in `reti` to `exec_builtin()`.  With that - code is broken - review done.

        int * reti;
        if (exec_builtin(reti, cmd)) {
            return 0;
        }