XOR linked list implementation - Follow Up
Don't use a different name for a struct and its typedef
Why is struct StNode typedef'ed to StHexNode? What does the Hex mean here, I don't see anything hexadecimal or hexagonal in the rest of the code? You can use exactly the same name for a struct as for its typedef, so I would just use that:
typedef struct StNode StNode;

You can also combine the typedef with the struct definition:
typedef struct StNode {
    ...
} StNode;

Use typedefs consistently
I see you use StHexNode in some places, and struct StNode in others. Be consistent and only use the typedef'ed variant.
Avoid repeating type names if possible
In this line:
StHexNode *newNode = malloc(sizeof(struct StNode));

Besides the inconstent use of the typedef, you repeat the type twice. That makes this error prone (if you make a mistake on the right side it will compile without errors but it will probably be wrong), and if you ever have to change the type of the variable newNode, you would have to do it in at least two places. It is better to avoid repeating the type name, but instead repeat the variable name:
StNode *newNode = malloc(sizeof(*newNode));

Prefer using compound assignment operators to avoid repetition
Use compound assignment operators if possible to save some typing, and also avoiding possible mistakes. For example, instead of:
lastNode-&gt;both = (uintptr_t)newNode ^ lastNode-&gt;both;

Prefer:
lastNode-&gt;both ^= (uintptr_t)newNode;

Simplifying get()
You can simplify the function get() somewhat. In particular, when looping over the elements of a list, try to start at index 0, and avoid making the start and end special cases. You can do this here like so:
StNode *get(StNode *headNode, unsigned int index)
{   
    StNode *currNode = headNode;
    uintptr_t prev = 0;
    
    for (int i = 0; i &lt; index; i++)
    {
        uintptr_t next = currNode-&gt;both ^ prev;
        prev = (uintptr_t)currNode;
        currNode = (StNode *)(next);
    }

    return currNode;
}

Note that you could even avoid the declaration of currNode if you change the name of headNode to currNode, but I would personally keep it as it is above, as it makes the role of the parameter and the local variable more clear.
Simplifying free_list()
The same goes for free_list(): you should not need to make a list of one element a special case. Also, why is free_list() calculating the number of elements of a list that will have been deleted by the time it returns?
void free_list(StNode *headNode)
{
    StNode *currNode = headNode;
    uintptr_t prev = 0;
    
    while (currNode)
    {
        uintptr_t next = currNode-&gt;both ^ prev;
        prev = (uintptr_t)currNode;
        free(currNode);
        currNode = (StNode *)(next);
    }
}

Use a unique and consistent naming scheme
If you want to use your XOR linked list in a real program, consider that names like StNode and get() are very generic and will likely conflict with other parts of a larger project. Maybe you need a binary tree implementation as well for example, and how are you going to name its function to retrieve an element at a given index? To solve this issue in C, come up with a unique prefix that you can use for all the struct and function names. For example, prefix everything with xllist_:
typedef struct xllist_node {
    ...
} xllist_node;

xllist_node *xllist_add(xllist_node *lastNode, int value);
xllist_node *xllist_get(xllist_node *headNode, usigned int index);
void xllist_free(xllist_node *headNode);

Of course you can argue about what the prefix should be exactly. I find something like xor_linked_list or XorLinkedList a bit too verbose, so xllist is a compromise: it still has list clearly in the name, and if you don't know what xl is you can look it up, and once you have seen what it means it is easy to remember that xl stands for XOR linked I hope.
Create a struct representing the whole list
You have a struct for a node, but not one for the whole list. That means the caller of your functions has to manually allocate the first list element, and it needs to keep track of both the head and tail node. It is much better if you create a struct representing the list:
typedef struct xllist {
    xllist_node *head;
    xllist_node *tail;
} xllist;

And then pass a pointer to this struct to functions like xllist_get(), xllist_add() and xllist_free(), like so:
xllist_node *xllist_add(xllist *list, int value) {
    xllist_node *newNode = malloc(sizeof(*newNode));
    newNode-&gt;both = (uintptr_t)xllist-&gt;tail;
    newNode-&gt;value = value;

    if (xllist-&gt;tail) {
        // Append it to the existing tail node
        xllist-&gt;tail-&gt;both ^= (uintptr_t)newNode;
        xllist-&gt;tail = newNode;
    } else {
        // The list was empty
        xllist-&gt;head = newNode;
        xllist-&gt;tail = newNode;
    }

    return newNode;
}

And you use it like so in main():
xllist myList = {NULL, NULL}; // declare an empty list

for (int i = 100; i &lt; 200; i++)
{
    xllist_add(&amp;myList, i);
}