FBI Vault Monitor with Desktop Alert

Build a Python tool that scrapes FBI FOIA releases and sends animated alerts.

Introduction

30 Second Summary

Government agencies quietly release documents all the time. Unless you check their websites every single day, you will miss what drops.

In this project, you will build a Python script that watches the FBI's public records vault for new document releases, detects changes automatically, and pops up a custom animated alert on your desktop when something new appears.

What You'll Build

You sit down at your computer in the morning, and a sleek dark window with the FBI logo slides onto your screen from the right, showing you the Bureau just dropped three new documents overnight. Then it slides back out on its own.

By the end of this project, you'll have:

  • A web scraper that pulls live document titles from the FBI Vault's recently-added page and prints them in your terminal.
  • A JSON-based change detection system that remembers what it saw last time and only fires an alert when genuinely new entries appear.
  • A custom animated desktop notification built with tkinter that slides in from the right edge of your screen, displays the new documents, then slides back out and disappears.
  • A daily cron job that runs the entire monitor in the background so you never have to remember to check manually.
  • Secret Mission: Add keyword filtering so you only get alerts for topics you care about.

Are there any prerequisites?

None. This guide walks you through installing everything you need. You just need a computer running Ubuntu with Python already installed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Project

Your FBI Vault monitor needs two Python libraries to do its job. requests fetches web pages from the FBI Vault, and beautifulsoup4 parses the HTML so you can extract document titles from the page.

In this step, you'll create a project folder, install both libraries, and confirm your environment is ready to scrape.

In this step, get ready to:
  • Create your project folder and open it in VS Code.
  • Install the web scraping Python packages.
  • Verify everything works together.
Create your project folder and open it in VS Code

You need a dedicated folder for your monitor script, its saved data, and the FBI logo it will download later.

  • Press Ctrl+Alt+T to open a terminal.
  • Create your project folder and move into it by running this command:
mkdir ~/fbi-vault-monitor && cd ~/fbi-vault-monitor
  • Open the folder in VS Code by running:
code .

VS Code opens with fbi-vault-monitor shown in the Explorer sidebar on the left.

VS Code didn't open?

If you see command not found, VS Code's CLI is not in your PATH.

  • Open VS Code manually by pressing the Super key, typing Visual Studio Code, and pressing Enter.
  • Use File > Open Folder and select fbi-vault-monitor in your home directory.

help me fix the code command so VS Code opens from the terminal

Install the required Python packages

The requests library handles HTTP connections to the FBI Vault website. beautifulsoup4 turns raw HTML into a searchable structure you can pull document titles from.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Install both packages by running:
pip install requests==2.34.2 beautifulsoup4==4.15.0

You should see download progress bars followed by a message confirming both packages were installed successfully.

Seeing a permission error or command not found?

  • If pip is not recognized, try pip3 instead. On some Ubuntu systems the command is named pip3 for Python 3.
  • If you see a permissions error, add --user at the end of the command to install into your user directory.

help me install Python packages on Ubuntu

Verify the installation

One quick command confirms that both libraries imported correctly and that tkinter (the GUI library you'll use for the animated alert) is available. tkinter ships with Python 3.14 on Ubuntu, so no extra install is needed.

  • Before you run this, what do you think will happen if one of the imports fails?
  • Confirm all three libraries are available by running:
python3 -c "import requests; from bs4 import BeautifulSoup; import tkinter; print('Ready!')"

You should see Ready! printed in your terminal. That single word proves requests, beautifulsoup4, and tkinter all imported without errors.

Seeing an import error?

  • If the error mentions requests or bs4, the pip install did not target the same Python version. Re-run the install as python3 -m pip install requests==2.34.2 beautifulsoup4==4.15.0 to force it through the correct interpreter.
  • If the error mentions tkinter, install the system package by running sudo apt install python3-tk and try again.

help me fix my Python import errors

Your environment is locked and loaded. Next up, you'll write the scraper that pulls real document titles straight from the FBI Vault.

Fetch and Parse the FBI Vault

Your environment is ready with all the libraries you need. Now it is time to prove you can actually pull real data from the FBI Vault and make sense of it.

Before you can detect changes or build notifications, you need to confirm you can reach the FBI Vault's Recently Added page and extract document titles from the HTML. This step gives you a visible list of real FBI documents printed directly in your terminal.

In this step, get ready to:
  • Create vault_monitor.py with imports and configuration constants.
  • Write a function that fetches and parses the FBI Vault's Recently Added page.
  • Run the script and see real FBI document titles printed in your terminal.
Set up the script file with imports and configuration

Every Python script starts with its imports and configuration values. You will define the FBI Vault URL and file paths the script will use throughout the project.

  • Create a new file called vault_monitor.py in VS Code by clicking File then New File.
  • Save it immediately as vault_monitor.py in your fbi-vault-monitor folder.
  • Add the following imports and configuration to the file:
#!/usr/bin/env python3
"""FBI Vault Monitor - Checks for new documents and shows animated desktop alert."""

import json
import os
import sys
import tkinter as tk
from pathlib import Path

import requests
from bs4 import BeautifulSoup

# Configuration
VAULT_URL = "https://vault.fbi.gov/recently-added"
SCRIPT_DIR = Path(__file__).parent
BASELINE_FILE = SCRIPT_DIR / "baseline.json"
LOGO_FILE = SCRIPT_DIR / "fbi_logo.png"
LOGO_URL = "https://vault.fbi.gov/images/fbi-logo.png"

What does this code do?

  • The shebang line #!/usr/bin/env python3 lets you run the script directly from cron later without specifying the Python path each time.
  • VAULT_URL points to the FBI Vault's Recently Added page where new FOIA documents appear.
  • SCRIPT_DIR uses Path(__file__).parent so all file paths are relative to wherever the script lives, not your current working directory.
  • BASELINE_FILE, LOGO_FILE, and LOGO_URL are paths and URLs you will use in later steps for change detection and the notification window.
  • Save vault_monitor.py.
  • Confirm your imports are correct by running this command in your terminal:
python3 vault_monitor.py

You should see no output and no errors. That silence means all your imports resolved correctly.

Seeing a ModuleNotFoundError?

  • If you see ModuleNotFoundError: No module named 'requests', run pip install requests==2.34.2 beautifulsoup4==4.15.0 again.
  • Make sure your terminal is in the ~/fbi-vault-monitor folder where vault_monitor.py lives.

Help me fix this ModuleNotFoundError in my FBI Vault Monitor script.

Write the fetch and parse function

The core of your scraper is a function that sends an HTTP request to the FBI Vault, then uses BeautifulSoup to pull structured data out of the raw HTML.

  • Add the fetch_recent_documents() function below your configuration constants:
def fetch_recent_documents():
    """Fetch and parse the FBI Vault Recently Added page."""
    response = requests.get(VAULT_URL, timeout=30)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    entries = []

    # Look for list items in the main content area
    content = soup.find("div", id="content-core")
    if content:
        for item in content.find_all("li"):
            text = item.get_text(strip=True)
            if text:
                entries.append(text)

    # Fallback: search broader content area
    if not entries:
        for item in soup.find_all("li"):
            text = item.get_text(strip=True)
            if text and len(text) > 10:
                entries.append(text)

    return entries

What does this function do?

  • requests.get(VAULT_URL, timeout=30) fetches the full HTML page from the FBI Vault. The timeout=30 prevents the script from hanging forever if the server is slow.
  • raise_for_status() throws an error if the request failed (like a 404 or 500), so you find out immediately rather than parsing garbage HTML.
  • The function first tries to find entries inside a div with id="content-core" (the main content area). If that finds nothing, it falls back to scanning all <li> elements on the page.
  • The fallback filter (len(text) > 10) skips tiny navigation items and only keeps real document titles.
  • Save vault_monitor.py. The function is defined and ready to call. The next section adds the main() function that uses it to print results.
Add the main entry point and run the script
  • Add the main() function and entry point below fetch_recent_documents():
def main():
    """Main function to check for new FBI Vault documents."""
    print("FBI Vault Monitor - Checking for new documents...")

    current_entries = fetch_recent_documents()

    if not current_entries:
        print("Warning: No entries found. The page structure may have changed.")
        return

    print(f"Found {len(current_entries)} entries on the Recently Added page.")
    for i, entry in enumerate(current_entries, 1):
        print(f"{i}. {entry}")


if __name__ == "__main__":
    main()

What does this code do?

  • main() calls your fetch function, checks that it actually found entries, then prints each one with a number.
  • The if not current_entries guard gives you a clear warning if the FBI changes their page layout, instead of silently doing nothing.
  • if __name__ == "__main__" means main() only runs when you execute the file directly, not when it gets imported by another script.
  • Save vault_monitor.py.

Before you run the script, what do you think will appear in the terminal? Will you see document titles, or might something go wrong with the network request?

  • Run the script to fetch real data from the FBI Vault:
python3 vault_monitor.py

You should see a numbered list of 10+ recently added FBI documents in your terminal. Each entry includes a date and document title, like 1. 06.11.26 Body-Worn Cameras Policy Guide 1343PG (Final).

Seeing a ConnectionError or timeout?

  • Check your internet connection. The script needs to reach vault.fbi.gov.
  • If you see HTTPError: 500, the FBI Vault server may be temporarily down. Wait a few minutes and try again.
  • If you see the "Warning: No entries found" message, the page loaded but parsing found nothing. This means the site structure may have changed.

Help me troubleshoot my FBI Vault Monitor script that is not returning document entries.

✔️ Awesome, I've got everything!

Great. Double check you have saved vault_monitor.py and that running it prints a numbered list of FBI documents.

ⓧ I'd like to double check the full code

Scroll up and confirm your vault_monitor.py contains these three sections in order:

  • Shebang, docstring, imports, and configuration constants (VAULT_URL, SCRIPT_DIR, BASELINE_FILE, LOGO_FILE, LOGO_URL).
  • The fetch_recent_documents() function with the content-core lookup and fallback logic.
  • The main() function and the if __name__ == "__main__" entry point at the bottom.

You have a working scraper that pulls real document titles from the FBI Vault. Next up, you will build the signature feature of this project: a branded, animated notification window that slides onto your screen when new documents are found.

Build the Animated FBI Alert Window

Your scraper pulls real FBI documents from the vault. Now you need something that actually catches your eye when new documents drop.

A terminal printout is easy to miss. In this step, you will build a custom notification window that slides onto your screen with the FBI logo, a bold red title, and a preview of what was just released. This is the signature feature of your monitor.

In this step, get ready to:
  • Download the FBI logo and build a logo-fetching function.
  • Create a borderless, dark-themed notification window with tkinter.
  • Add smooth slide-in and slide-out animation to the alert.
Add the logo download function

The notification window will display the FBI logo for branding. Your script needs to download it once from the vault and save it locally so it does not re-fetch every run.

  • In VS Code, open vault_monitor.py.
  • Find the line that says def fetch_recent_documents():. Add the following function directly above it:
def download_logo():
    """Download the FBI logo if not already present."""
    if not LOGO_FILE.exists():
        print("Downloading FBI logo...")
        response = requests.get(LOGO_URL, timeout=30)
        response.raise_for_status()
        with open(LOGO_FILE, "wb") as f:
            f.write(response.content)
        print("Logo saved.")

What does this code do?

  • LOGO_FILE.exists() checks whether the logo has already been downloaded. If it has, the function skips the network request entirely.
  • requests.get(LOGO_URL, timeout=30) fetches the PNG image from the FBI Vault website.
  • The file is written in binary mode ("wb") because images are binary data, not text.
  • Now scroll down to the main() function. Find this line:
    print("FBI Vault Monitor - Checking for new documents...")
  • Add a call to download_logo() directly below it, so the top of main() now looks like this:
    print("FBI Vault Monitor - Checking for new documents...")

    download_logo()
  • Save vault_monitor.py.
  • Run the script to confirm the logo downloads by running this command:
python3 vault_monitor.py
  • You should see "Downloading FBI logo..." followed by "Logo saved." in the output, then the list of recently added documents.
  • Confirm the logo file exists by running:
ls

You should see fbi_logo.png listed alongside vault_monitor.py.

Not seeing the logo file?

  • Make sure you placed download_logo() above fetch_recent_documents() in the file. Python reads top to bottom, so functions must be defined before they are called.
  • If you see a connection error, check your internet connection. The script needs to reach vault.fbi.gov.
  • Make sure the download_logo() call is inside main() (indented under it), not at the top level of the file.

Help me debug why my FBI logo is not downloading.

Create the notification window

With the logo saved, you can now build the notification window itself. You will use tkinter to create a borderless, dark-themed popup that floats above all other windows.

  • In vault_monitor.py, find the line def fetch_recent_documents():. Add the start of show_notification() directly above it (below download_logo()):
def show_notification(new_entries):
    """Show an animated sliding notification window with FBI logo."""
    root = tk.Tk()
    root.overrideredirect(True)
    root.attributes("-topmost", True)

    # Window dimensions
    win_width = 420
    win_height = 180
    screen_width = root.winfo_screenwidth()

    # Start off-screen to the right
    start_x = screen_width
    y_pos = 50
    end_x = screen_width - win_width - 20

    root.geometry(f"{win_width}x{win_height}+{start_x}+{y_pos}")
    root.configure(bg="#1a1a2e")

    # Load FBI logo
    logo_image = None
    try:
        logo_image = tk.PhotoImage(file=str(LOGO_FILE))
        logo_image = logo_image.subsample(2, 2)
    except tk.TclError:
        pass

    # Layout
    frame = tk.Frame(root, bg="#1a1a2e", padx=15, pady=10)
    frame.pack(fill=tk.BOTH, expand=True)

What does this code do?

  • overrideredirect(True) removes the window's title bar and borders, creating a sleek frameless popup.
  • attributes("-topmost", True) keeps the window above all other windows so the learner never misses it.
  • The window starts at start_x = screen_width which places it just off the right edge of the screen (invisible). This is the starting position for the slide animation.
  • subsample(2, 2) scales the logo down by half so it fits neatly in the notification header.
  • Directly below the frame.pack(fill=tk.BOTH, expand=True) line you just added, paste the header widgets and a temporary root.mainloop() to test:
    # Header with logo and title
    header_frame = tk.Frame(frame, bg="#1a1a2e")
    header_frame.pack(fill=tk.X, pady=(0, 8))

    if logo_image:
        logo_label = tk.Label(header_frame, image=logo_image, bg="#1a1a2e")
        logo_label.image = logo_image
        logo_label.pack(side=tk.LEFT, padx=(0, 10))

    title_label = tk.Label(
        header_frame,
        text="FBI VAULT ALERT",
        font=("Helvetica", 14, "bold"),
        fg="#e94560",
        bg="#1a1a2e",
    )
    title_label.pack(side=tk.LEFT)

    # Close button
    close_btn = tk.Label(
        header_frame,
        text="X",
        font=("Helvetica", 12, "bold"),
        fg="#ffffff",
        bg="#1a1a2e",
        cursor="hand2",
    )
    close_btn.pack(side=tk.RIGHT)
    close_btn.bind("<Button-1>", lambda e: root.destroy())

    root.mainloop()

What does this code do?

  • The header frame holds the logo on the left and the title on the right, arranged horizontally with side=tk.LEFT.
  • logo_label.image = logo_image keeps a reference to the image object. Without this, Python's garbage collector would delete the image from memory and it would vanish from the window.
  • The close button is a clickable Label (styled as "X") that destroys the window when clicked.
  • root.mainloop() starts the tkinter event loop and keeps the window on screen until it is closed or destroyed.
  • Now scroll to main() at the bottom of the file. Find the for loop that prints entries:
    for i, entry in enumerate(current_entries, 1):
        print(f"{i}. {entry}")
  • Replace those two lines with a temporary test call to your notification function:
    # Temporary test call
    show_notification(["Test Document Part 01", "Another Test File (Final)"])
  • Save vault_monitor.py.
  • Run the script to test the static window by running:
python3 vault_monitor.py

A dark, borderless window should appear in the top-right area of your screen showing the FBI logo and "FBI VAULT ALERT" in red text. Click the X to close it.

No window appearing?

  • If you see an error about tkinter, run sudo apt install python3-tk and try again.
  • Make sure show_notification() is defined above fetch_recent_documents() and above main() in the file. Python needs functions defined before they are called.
  • If the window appears but has no logo, that is OK for now. The logo may not render if the PNG format is unsupported on your system.

Help me debug why my tkinter notification window is not appearing.

Add the slide animation

A static popup works, but a notification that slides in from the edge of the screen is much harder to ignore. You will use tkinter's after() method to schedule repeated position updates that create smooth motion.

  • In show_notification(), find the line root.mainloop() at the bottom of the function. Add the message and preview labels directly above it:
    # Message
    count = len(new_entries)
    msg_text = f"{count} new document{'s' if count != 1 else ''} added to the FBI Vault!"
    msg_label = tk.Label(
        frame,
        text=msg_text,
        font=("Helvetica", 11),
        fg="#ffffff",
        bg="#1a1a2e",
        wraplength=380,
        justify=tk.LEFT,
    )
    msg_label.pack(fill=tk.X, pady=(0, 5))

    # Preview of entries
    preview = "\n".join(new_entries[:3])
    if count > 3:
        preview += f"\n... and {count - 3} more"

    preview_label = tk.Label(
        frame,
        text=preview,
        font=("Helvetica", 9),
        fg="#a0a0a0",
        bg="#1a1a2e",
        wraplength=380,
        justify=tk.LEFT,
    )
    preview_label.pack(fill=tk.X)

What does this code do?

  • The message label builds a dynamic string like "2 new documents added to the FBI Vault!" based on how many entries are in the list.
  • The preview label shows up to 3 document titles in grey text. If there are more than 3, it appends a "... and N more" line.
  • wraplength=380 ensures long document titles wrap within the window instead of getting cut off.
  • Now find root.mainloop() again (it should be right below the preview_label code you just added). Add the animation logic directly above it:
    # Animation variables
    current_x = start_x

    def slide_in():
        nonlocal current_x
        if current_x > end_x:
            current_x -= 15
            root.geometry(f"{win_width}x{win_height}+{current_x}+{y_pos}")
            root.after(10, slide_in)
        else:
            root.after(8000, slide_out)

    def slide_out():
        nonlocal current_x
        if current_x < screen_width:
            current_x += 15
            root.geometry(f"{win_width}x{win_height}+{current_x}+{y_pos}")
            root.after(10, slide_out)
        else:
            root.destroy()

    root.after(100, slide_in)

How does the animation work?

  • slide_in() moves the window 15 pixels to the left every 10 milliseconds. It calls itself recursively using root.after(10, slide_in) until the window reaches its target position.
  • Once the window arrives, it waits 8 seconds (root.after(8000, slide_out)) so you can read the content.
  • slide_out() reverses the process, pushing the window back off-screen to the right. Once it is fully off-screen, root.destroy() closes the window and ends the program.
  • root.after(100, slide_in) kicks off the first animation frame 100ms after the window is created, giving tkinter time to render everything first.

Before you run this, what do you think will happen? Will the window appear instantly, or will it move onto the screen?

  • Save vault_monitor.py and run the full test:
python3 vault_monitor.py

A dark notification window should slide in smoothly from the right edge of your screen. It displays the FBI logo, "FBI VAULT ALERT" in red, a message about 2 new documents, and the test document titles in grey. After 8 seconds, it slides back out and the script exits.

Window appears but does not slide?

  • Make sure root.after(100, slide_in) appears directly above root.mainloop() at the bottom of show_notification(). If it is missing, the animation never starts.
  • Check that current_x = start_x is set before the slide_in() definition. Without the starting position, the animation has nothing to decrement.
  • If the window slides in but never slides out, confirm that root.after(8000, slide_out) is inside the else branch of slide_in().

Help me debug why my notification window animation is not working.

✔️ Awesome, I've got everything!

Great. Double check you have saved vault_monitor.py and that the notification slides in and out when you run the script.

ⓧ I'd like to double check the full code

Your vault_monitor.py should contain these sections in order. Scroll through your file and confirm each is present:

  • Imports (json, os, sys, tkinter, Path, requests, BeautifulSoup).
  • Configuration constants (VAULT_URL, SCRIPT_DIR, BASELINE_FILE, LOGO_FILE, LOGO_URL).
  • download_logo() function that downloads the PNG if not already present.
  • show_notification() function with: window setup, logo loading, header frame with logo and title, close button, message label, preview label, slide_in/slide_out animation, root.after(100, slide_in), and root.mainloop().
  • fetch_recent_documents() function (unchanged from Step 2).
  • main() function calling download_logo(), fetch_recent_documents(), and show_notification() with the test list.

Your FBI Vault Monitor now has its signature look. Next up, you will give it memory so it only fires when genuinely new documents appear.

Add Change Detection

Your script can fetch documents from the FBI Vault and flash a slick animated notification. But it has no memory.

Every time you run it, it treats every document as brand new and fires the alert. Without change detection, you would see the same notification for the same documents every single day. In this step, you will give the script a memory by saving what it last saw to a JSON file, then only alerting when something genuinely new appears.

In this step, get ready to:
  • Add baseline functions for loading, saving, and comparing document lists.
  • Wire the change detection into the main script flow.
  • Test the full cycle: first run, second run, and simulated new document.
Add JSON baseline functions

Your script needs three capabilities to detect changes: loading a previously saved list, saving the current list, and comparing the two to find differences.

  • In vault_monitor.py, find the fetch_recent_documents() function. Add the following three functions directly below it (after its closing return entries line):
def load_baseline():
    """Load the previously saved baseline entries."""
    if BASELINE_FILE.exists():
        with open(BASELINE_FILE, "r") as f:
            return json.load(f)
    return None


def save_baseline(entries):
    """Save current entries as the new baseline."""
    with open(BASELINE_FILE, "w") as f:
        json.dump(entries, f, indent=2)


def find_new_entries(current, baseline):
    """Find entries in current that are not in baseline."""
    baseline_set = set(baseline)
    return [entry for entry in current if entry not in baseline_set]

What does this code do?

  • load_baseline() reads baseline.json and returns the saved list of document titles. If the file does not exist yet (first run), it returns None.
  • save_baseline() writes the current entries list to baseline.json so the script remembers what it saw.
  • find_new_entries() converts the baseline into a set for fast lookup, then returns only the entries that are NOT in that set. This is how the script identifies genuinely new documents.
  • Save vault_monitor.py.
  • These functions are ready to use. In the next substep, you will wire them into main() and see the result.
Wire the change detection into the main flow

Now you need to replace the hardcoded test call in main() with real logic that loads the baseline, compares entries, and decides whether to notify.

  • In vault_monitor.py, scroll down to the main() function and find these two lines at the bottom:
    # Temporary test call
    show_notification(["Test Document Part 01", "Another Test File (Final)"])
  • Delete those two lines and replace them with the following change detection logic:
    # Load baseline
    baseline = load_baseline()

    # First run: save baseline without notifying
    if baseline is None:
        print("First run detected. Saving baseline (no notification).")
        save_baseline(current_entries)
        print("Baseline saved. You will be notified of new entries on the next run.")
        return

    # Find new entries
    new_entries = find_new_entries(current_entries, baseline)

    if new_entries:
        print(f"New documents found: {len(new_entries)}")
        for entry in new_entries:
            print(f"  - {entry}")

        # Update baseline
        save_baseline(current_entries)

        # Show notification
        show_notification(new_entries)
    else:
        print("No new documents since last check.")

What does this code do?

  • If no baseline file exists (first run), the script saves the current entries as the baseline and exits without showing a notification. This prevents a flood of alerts on the very first execution.
  • On subsequent runs, it compares the current entries against the saved baseline. If new entries are found, it prints them, updates the baseline, and fires the notification.
  • If nothing has changed, it prints a simple message and exits quietly.
  • Save vault_monitor.py.

Seeing a syntax error?

  • Make sure you deleted both lines of the temporary test call (the comment and the show_notification() call with hardcoded data).
  • Check that the new code is indented with 4 spaces to match the rest of the main() function body.
  • Confirm the three baseline functions (load_baseline, save_baseline, find_new_entries) are placed ABOVE the show_notification() function, not inside it.

Help me debug a syntax error in my vault_monitor.py change detection code.

Test the full flow

Time to prove the memory works. You will run the script three times, each demonstrating a different scenario.

  • First, make sure no baseline.json file exists yet. Delete it if it does by running:
rm -f ~/fbi-vault-monitor/baseline.json

Before you run the script, what do you think will happen on this first execution? Will a notification appear?

  • Run the script for the first time:
python3 vault_monitor.py

You should see output like this:

FBI Vault Monitor - Checking for new documents...
Found 15 entries on the Recently Added page.
First run detected. Saving baseline (no notification).
Baseline saved. You will be notified of new entries on the next run.

Why no notification on the first run?

If the script alerted on first run, it would treat every existing document as "new" and fire a massive notification for old content. The first run establishes a baseline so that future runs can compare against it.

  • Confirm baseline.json was created by running:
ls ~/fbi-vault-monitor/baseline.json

You should see the file listed. Now run the script a second time:

python3 vault_monitor.py

This time you should see:

FBI Vault Monitor - Checking for new documents...
Found 15 entries on the Recently Added page.
No new documents since last check.

No notification appeared because nothing changed. The baseline matches the current state of the page.

  • Now simulate a new document appearing. Open baseline.json in VS Code and delete any one entry from the list (remove the entire line including the quotes and comma). Save the file.
  • Run the script one more time:
python3 vault_monitor.py

This time the script detects that an entry exists on the page which is NOT in the baseline. You should see output naming the "new" document, followed by the FBI alert notification sliding onto your screen.

No notification appeared after editing the baseline?

  • Make sure you saved baseline.json after removing the entry.
  • Verify the JSON is still valid. A trailing comma or missing bracket will cause a parse error. Look for an error message in the terminal output.
  • Confirm you removed a complete entry (the full string between quotes), not just part of one.

Help me figure out why my baseline edit did not trigger a notification.

✔️ Awesome, I've got everything!

Great. Double check you have saved vault_monitor.py with all changes.

ⓧ I'd like to double check the full code

Your vault_monitor.py should now contain these sections in order. Scroll through and confirm each one is present:

  • Imports and configuration constants (VAULT_URL, SCRIPT_DIR, BASELINE_FILE, LOGO_FILE, LOGO_URL).
  • download_logo() function.
  • fetch_recent_documents() function with the content-core lookup and fallback.
  • load_baseline() function that reads baseline.json or returns None.
  • save_baseline() function that writes entries to baseline.json.
  • find_new_entries() function using set difference.
  • show_notification() function with the full slide animation.
  • main() function with first-run detection, baseline comparison, and conditional notification. No hardcoded test call.
  • if __name__ == "__main__": block at the bottom.

Your monitor now has a memory. It knows what it saw last time and only bothers you when something genuinely new appears. Next up, you will set this script to run automatically every day so you never have to think about it again.

Schedule with Cron

Your FBI Vault Monitor can fetch documents, detect changes, and slide a branded alert onto your screen. There is one problem: you still have to remember to run it yourself.

A cron job solves this by running the script automatically on a schedule you choose. In this step, you will make the script executable and register it with cron to run once daily at 9:00 AM.

In this step, get ready to:
  • Make the script executable so cron can run it directly.
  • Add a daily cron job that triggers the monitor at 9:00 AM.
  • Verify the cron entry is installed and ready to fire.
Make the script executable

For cron to run your script directly, the file needs two things: a shebang line that tells the system which interpreter to use, and the executable permission flag set on the file.

  • In VS Code, open vault_monitor.py and confirm the very first line reads:
#!/usr/bin/env python3

What is a shebang line?

The #! sequence at the top of a script tells Linux which program should interpret the file. When you mark the file as executable, the system reads this line and launches python3 automatically.

  • Mark the script as executable by running this command in your terminal:
chmod +x ~/fbi-vault-monitor/vault_monitor.py
  • Confirm python3 is available at /usr/bin/python3 by running:
which python3

You should see /usr/bin/python3 printed. This is the full path you will use in the cron entry.

Seeing a different path?

If which python3 shows a different location (like /usr/local/bin/python3), use that path in your cron entry instead of /usr/bin/python3. The path must point to your actual Python 3 binary.

Need help? Why is my python3 path different from /usr/bin/python3?

Add a daily cron job

The crontab is a per-user file that tells cron which commands to run and when. You will add one line that fires your monitor script every morning.

  • First, confirm your username by running:
whoami

Note the output. You will substitute it into the cron entry below.

  • Open your crontab for editing by running:
crontab -e

First time opening crontab?

If this is your first time running crontab -e, it may ask you to choose an editor. Select nano (usually option 1). Nano is the simplest terminal text editor.

  • Scroll to the bottom of the file and add this line, replacing your-username with the output from whoami:
0 9 * * * DISPLAY=:0 /usr/bin/python3 /home/[[UBUNTU_USERNAME="your-username"]]/fbi-vault-monitor/vault_monitor.py >> /tmp/fbi_vault_monitor.log 2>&1
  • Save and exit nano by pressing Ctrl+O (write out), then Enter to confirm the filename, then Ctrl+X to exit.

What does this cron line do?

  • 0 9 * * * means "at minute 0, hour 9, every day of the month, every month, every day of the week." That is 9:00 AM daily.
  • DISPLAY=:0 tells tkinter which screen to draw on. Without this, cron cannot open GUI windows.
  • >> /tmp/fbi_vault_monitor.log 2>&1 appends all output (including errors) to a log file so you can debug later.

Crontab not saving?

Make sure the cron line is on its own line with no leading spaces. Some editors add a trailing newline at the end of the file, which is fine. If you see an error about a bad minute or syntax, double-check that 0 9 * * * has spaces between each field (five fields total before the command).

Still stuck? Help me fix my crontab syntax for the FBI Vault Monitor.

Verify the cron job is installed

Before you walk away and trust cron to do its job, confirm the entry actually saved.

  • Before you run the next command, what do you expect to see? Think about what crontab -l should output if your entry saved correctly.
  • List your cron entries by running:
crontab -l

You should see your FBI Vault Monitor line printed, with the 0 9 * * * schedule and the full path to your script.

Test it without waiting until tomorrow

To confirm the cron job actually fires, temporarily change 0 9 to 2 minutes from now (e.g. 35 14 for 2:35 PM). Wait for that time to pass, then check the log file:

cat /tmp/fbi_vault_monitor.log

If you see the script's output in the log, cron is working. Change the time back to 0 9 when you are done testing.

Log file empty or missing?

If the log file does not appear after the scheduled time, check that the Python path in your cron entry matches the output of which python3. Also verify your username is spelled correctly in the script path.

If the log exists but contains an error about tkinter or display, confirm that DISPLAY=:0 is present in the cron line and that you are logged into your desktop session.

Need help? My cron job for the FBI Vault Monitor is not running or producing errors.

Secret mission

Add Keyword Filtering

The FBI Vault releases dozens of documents every week. Most of them probably have nothing to do with your interests. What if you could configure a list of keywords and only get alerted when something matching your topics drops?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses no cloud infrastructure. There are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The ~/fbi-vault-monitor project folder (contains vault_monitor.py, baseline.json, fbi_logo.png, keywords.json).
  • A cron job entry running the monitor daily at 9:00 AM.
  • The requests and beautifulsoup4 Python packages.
  • A log file at /tmp/fbi_vault_monitor.log (created after the first cron run).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep receiving daily FBI Vault alerts.

  • Your cron job continues to run daily at 9:00 AM and shows a notification when new documents appear.
  • The requests and beautifulsoup4 packages remain installed in your Python environment.

Pause - I'll come back to this later

Disable the cron job so the monitor stops running daily. Your project files stay intact for when you return.

  • Open your crontab for editing by running:
crontab -e
  • Add a # at the start of the FBI Vault Monitor line to comment it out. The line should now look like:
# 0 9 * * * DISPLAY=:0 /usr/bin/python3 /home/$USER/fbi-vault-monitor/vault_monitor.py >> /tmp/fbi_vault_monitor.log 2>&1
  • Save and close the editor. The cron job is now paused.
  • To resume later, remove the # from the start of the line.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your project files, cron job, and log.

  • Remove the cron job by running:
crontab -e
  • Delete the FBI Vault Monitor line from the file, then save and close. If this is your only cron job, you can remove the entire crontab by running:
crontab -r
  • Delete the project folder and all its contents by running:
rm -rf ~/fbi-vault-monitor
  • Remove the log file by running:
rm -f /tmp/fbi_vault_monitor.log
  • Uninstall the Python packages by running:
pip uninstall requests beautifulsoup4 -y

Other projects may use these packages

If you have other Python projects that depend on requests or beautifulsoup4, skip the uninstall step. These are common libraries used by many Python scripts.

That's a Wrap!

That's a Wrap!

Nice work! You've built a fully automated surveillance tool that watches the FBI's public records vault and announces new document releases with a custom animated desktop alert.

You've learned how to:

  • Build a web scraper that pulls real-time data from a government website and detect new entries using a JSON-based change detection system.
  • Design a custom animated desktop notification using tkinter with no window borders, the FBI logo, and smooth slide-in/slide-out animation.
  • Automate the entire workflow with a daily cron job so the monitor runs hands-free every morning.
  • Secret Mission: Added keyword filtering to create a personalized intelligence feed from FBI document releases.

Ready to quiz yourself?