The loop is to check for prior existence in the list actively being built. If not there (found was never set true), the remaining inconveniently omitted code adds it to the list.
On initial run, the numbers linked list head pointer is null, signifying an empty list. That doesn't change the algorithm of search + if-not-found-insert whatsoever. It just means the loop is never entered because the bail-case is immediately true. in other words, with numbers being NULL
for (node *ptr = numbers; ptr != NULL; ptr = ptr->next)
the condition to continue, ptr != NULL is already false, so the body of the for-loop is simply skipped. That leads to the remainder of the code you didn't post, which does the actual insertion. After that insertion, the list now has something, and the next iteration of the outer-while loop will eventually scan the list again after the next prospect value is read. This continues until the outer-while condition is no longer satisfied.
A Different Approach
I have never been fond of the cs50 development strategy, and Harvard's technique for teaching C to entry-level CS students. The cs50 header and lib has caused more transitional confusion to real-world software engineering than one can fathom. Below is an alternative for reading a linked list of values, keeping only unique entries. It may look like a lot, but half of this is inline comments describing what is going on. Some of it will seem trivial, but the search-and-insert methodology is what you should be focusing on. It uses a strategy of pointer-to-pointer that you're likely not familiar with, and this is a good exposure.
Enjoy.
#include <stdio.h>
#include <stdlib.h>
struct node
{
int value;
struct node *next;
};
int main()
{
struct node *numbers = NULL;
int value = 0;
// retrieve list input. stop when we hit
// - anything that doesn't parse as an integer
// - a value less than zero
// - EOF
while (scanf("%d", &value) == 1 && value >= 0)
{
// finds the address-of (not the address-in) the first
// pointer whose node has a value matching ours, or the
// last pointer in the list (which points to NULL).
//
// note the "last" pointer will be the head pointer if
// the list is empty.
struct node **pp = &numbers;
while (*pp && (*pp)->value != value)
pp = &(*pp)->next;
// if we didn't find our value, `pp` holds the address of
// the last pointer in the list. Again, not a pointer to the
// last "node" in the list; rather the last actual "pointer"
// in the list. Think of it as the "next" member of last node,
// and in the case of an empty list, it will be the address of
// the head pointer. *That* is where we will be hanging our
// new node, and since we already know where it goes, there is
// no need to rescan the list again.
if (!*pp)
{
*pp = malloc(sizeof **pp);
if (!*pp)
{
perror("Failed to allocate new node");
exit(EXIT_FAILURE);
}
(*pp)->value = value;
(*pp)->next = NULL;
}
}
// display entire list, single line
for (struct node const *p = numbers; p; p = p->next)
printf("%d ", p->value);
fputc('\n', stdout);
// free the list
while (numbers)
{
struct node *tmp = numbers;
numbers = numbers->next;
free(tmp);
}
return EXIT_SUCCESS;
}
This approach is especially handy when building sorted lists, as it can be altered with just a few changes to do so.
foundwas never set true), the remaining inconveniently omitted code adds it to the list. On initial run, the numbers linked list head pointer is null, signifying an empty list. That doesn't change the algorithm of search + if-not-found-insert whatsoever. It just means the loop is never entered because the bail-case is immediately true.