Before you start
Three earlier labs have to be finished first. This one builds on all three and repeats none of them.
- Free Always-On Linux VM on Google Cloud — the
g1VM inus-central1-a, with nginx already serving a page over port 80. - Buy a Cheap Domain and Set Up a Subdomain — a domain on Namecheap BasicDNS, with A records for
@andlab. - Claude Code on Your Own Machine — Claude Code installed on your laptop and signed in on a Pro plan.
Open Claude Code on your laptop and ask it for your VM's external IP — you need it for the DNS record in step 1:
Prompt 1 — read the VM’s IP
Get the external IP of my Google Cloud VM g1 in zone us-central1-a with gcloud. Print only the IP address on its own line. Show me the command you ran and its raw output.استخرج عنوان IP الخارجي لآلتي الافتراضية g1 في المنطقة us-central1-a على Google Cloud باستخدام gcloud. اطبع عنوان IP وحده في سطر مستقل. واعرض لي الأمر الذي نفّذته وخرجه الخام.
gcloud compute ssh g1 --zone=us-central1-a --command "..." and gcloud compute scp. Nothing gets installed on the VM by hand, and Claude Code is never installed on the VM.Replace yourdomain.xyz with your own domain everywhere below.
1 — Add the phish subdomain at Namecheap
You need one new A record at Namecheap, next to the @ and lab records you already have. Pick one of two ways: let Claude in Chrome add it for you (option A), or add it by hand (option B).
Option A. This needs Claude in Chrome, set up in Claude in Chrome Setup. Log in to Namecheap in Chrome yourself first, then give Claude in Chrome this prompt, with the IP from Prompt 1 in place of YOUR_VM_IP:
Prompt 2 — option A: Claude in Chrome adds the record
I am logged in to Namecheap in this Chrome tab. Go to Domain List, click Manage next to yourdomain.xyz, and open Advanced DNS. Add one new record: type A Record, host phish, value YOUR_VM_IP, TTL Automatic. Save it. Do not change or delete any other record, and do not touch the nameservers. When you are done, show me the full list of host records as they look now.أنا مسجَّل الدخول إلى Namecheap في تبويب Chrome هذا. اذهب إلى Domain List، وانقر Manage بجانب yourdomain.xyz، وافتح Advanced DNS. أضف سجلاً جديداً واحداً: النوع A Record، والمضيف phish، والقيمة YOUR_VM_IP، و TTL على Automatic. ثم احفظه. لا تغيّر أي سجل آخر ولا تحذفه، ولا تلمس خوادم الأسماء. وحين تنتهي، اعرض لي القائمة الكاملة لسجلات المضيف كما تبدو الآن.
Option B — do it by hand instead
In the Namecheap dashboard: Domain List → Manage → Advanced DNS. Add one row next to the @ and lab records you already have:
| Type | Host | Value | TTL |
|---|---|---|---|
| A Record | phish | your server's IP | Automatic |
That gives you phish.yourdomain.xyz, pointing at the same VM as lab. Nothing else changes: the nameservers stay on Namecheap BasicDNS.
DNS is not instant — minutes usually. Back in Claude Code, check it from your laptop:
Prompt 3 — check the new name
Run dig +short phish.yourdomain.xyz and tell me whether it prints the external IP of g1. Then run curl -sI http://phish.yourdomain.xyz and tell me whether nginx answers. If the name does not resolve yet, wait one minute and try again, up to five times. Show me every command you ran and its raw output.نفّذ dig +short phish.yourdomain.xyz وأخبرني هل يطبع عنوان IP الخارجي للآلة g1. ثم نفّذ curl -sI http://phish.yourdomain.xyz وأخبرني هل يجيب nginx. إن لم يُحَلّ الاسم بعد، فانتظر دقيقة وأعد المحاولة، حتى خمس مرات. واعرض لي كل أمر نفّذته وخرجه الخام.
g1 running, or reserve a static IP.phish.yourdomain.xyz resolving to your VM's external IP, and nginx answering on http://phish.yourdomain.xyz.2 — Get the data
The dataset is the PhiUSIIL Phishing URL Dataset from the UCI Machine Learning Repository: about 235,000 URLs, roughly 50 features already computed for each one, and a label column saying phishing or benign. It downloads without a login.
Open Claude Code on your laptop and give it this:
Prompt 4 — download and inspect
Make a new folder ~/phish-ml and work inside it. Download this file: https://archive.ics.uci.edu/static/public/967/phiusiil+phishing+url+dataset.zip Unzip it, load the CSV with pandas, and tell me four things: how many rows there are, how many are phishing and how many are benign, the data type of every column, and whether any column has missing values. Do not train anything yet.أنشئ مجلداً جديداً باسم ~/phish-ml واعمل داخله. نزّل هذا الملف: https://archive.ics.uci.edu/static/public/967/phiusiil+phishing+url+dataset.zip فُكَّ ضغطه، وحمّل ملف CSV باستخدام pandas، ثم أخبرني أربعة أمور: عدد الصفوف، وكم منها phishing وكم منها حميد، ونوع البيانات لكل عمود، وهل يوجد في أي عمود قيم ناقصة. لا تدرّب أي نموذج بعد.
What to look for:
- Around 235,000 rows. A much smaller number means the zip did not extract fully.
- The two classes are close to balanced — neither is a rare event. That matters for how you read the scores in the next step.
- Most columns are numeric. A couple are text: the raw URL and the domain. Those are identifiers, not features, and they have to come out before training.
- No missing values. If Claude reports some, ask it which columns and how many before you go further.
~/phish-ml holds the extracted CSV, and you can state the row count and the class balance from memory.3 — Train the classifier
g1 has 1 GB of RAM and two shared vCPU bursts; it will not fit 235,000 rows and a random forest comfortably, and the swap file will thrash if you try. The VM's job is to serve one finished model file, which costs almost nothing.Prompt 5 — two models, one winner
In ~/phish-ml, train on that CSV with scikit-learn. Drop every column that is not a feature, including the raw URL text and the domain name, and use the label column as the target. Split the data 80/20, stratified on the label, with random_state=42. Train two models: logistic regression as the baseline, and a random forest. For each one, print accuracy, precision, recall, macro F1 and the confusion matrix on the test set. Keep the model with the better macro F1. Save it to model.joblib together with the exact list of feature column names in the order used for training, and print that list for me.في المجلد ~/phish-ml، درّب على ملف CSV باستخدام scikit-learn. احذف كل عمود ليس سمة، ومن ذلك نص الـ URL الخام واسم النطاق، واستخدم عمود label كهدف. اقسم البيانات 80/20 تقسيماً طبقياً على العمود label، مع random_state=42. درّب نموذجين: logistic regression كخط أساس، و random forest. لكل منهما اطبع accuracy و precision و recall و macro F1 ومصفوفة الالتباس على مجموعة الاختبار. احتفظ بالنموذج الأفضل في macro F1. احفظه في model.joblib مع القائمة الدقيقة لأسماء أعمدة السمات بالترتيب المستخدم في التدريب، واطبع لي هذه القائمة.
What to look for:
- Macro F1 is the number to read, not accuracy. Accuracy on this dataset flatters everything: a model that is lazy about one class still scores high, because both classes are large. Macro F1 averages the per-class F1 scores without weighting by class size, so a model that quietly gives up on phishing URLs cannot hide behind it.
- The confusion matrix tells you which mistake the model makes. A false negative is a phishing URL waved through; a false positive is a real site blocked. They are not equally expensive, and the matrix is where you see the trade.
- The random forest will almost certainly beat the baseline. Keep the baseline number anyway — without it you have nothing to say about whether the forest is worth its extra cost.
- A score near 100% should make you suspicious, not pleased. Ask Claude Code which features carry the most weight; if one feature alone explains the label, it is probably leaking the answer.
model.joblib exists in ~/phish-ml, and you can say the macro F1 of both models and which one got saved.4 — Wrap it in a web service
The model takes a row of numbers. A visitor types a URL. Something has to turn one into the other, and that something is the part most likely to go wrong.
Prompt 6 — the Flask app
In ~/phish-ml, write app.py: a small Flask app that loads model.joblib and the saved feature list. GET / shows one text box for a URL and a Submit button. POST / takes that URL, computes the same features the model was trained on in the exact order of the saved feature list, and shows "phishing" or "benign" with the model's probability as a percentage. GET /health returns JSON with the status, the model type and the number of features. Put the feature extraction in one function, and at start-up check that the names and order it produces match the saved feature list exactly — refuse to start if they do not. Keep it to app.py plus a requirements.txt. Then run it locally so I can try it at http://127.0.0.1:5000.في المجلد ~/phish-ml، اكتب الملف app.py: تطبيق Flask صغير يحمّل model.joblib وقائمة السمات المحفوظة. GET / يعرض مربع نص واحد لعنوان URL وزر إرسال. POST / يأخذ ذلك العنوان، ويحسب السمات نفسها التي درّب عليها النموذج وبالترتيب نفسه تماماً كما في قائمة السمات المحفوظة، ثم يعرض "phishing" أو "benign" مع احتمال النموذج كنسبة مئوية. GET /health يعيد JSON يحتوي الحالة ونوع النموذج وعدد السمات. اجعل استخراج السمات في دالة واحدة، وتحقق عند بدء التشغيل من أن الأسماء والترتيب الناتجين يطابقان قائمة السمات المحفوظة تماماً — وارفض التشغيل إن لم يطابقا. اقتصر على app.py وملف requirements.txt. ثم شغّله محلياً حتى أجرّبه على http://127.0.0.1:5000.
Now test it on URLs you can judge yourself: two real sites, and a made-up one with a bare IP for a hostname that looks like phishing. Claude Code also confirms the service answers a machine as well as a browser:
Prompt 7 — test it locally
The app from the last prompt is running at http://127.0.0.1:5000. Leave it running. Run curl -s http://127.0.0.1:5000/health. Then POST each of these three URLs to / the same way the form does, and tell me the verdict and the probability for each one: https://www.wikipedia.org https://www.python.org http://203.0.113.45/paypal.com-login/verify/account.php?session=99812 Show me every command you ran and its raw output.التطبيق من الطلب السابق يعمل على http://127.0.0.1:5000. اتركه يعمل. نفّذ curl -s http://127.0.0.1:5000/health. ثم أرسل كل عنوان من هذه العناوين الثلاثة بطلب POST إلى / بالطريقة نفسها التي يرسل بها النموذج، وأخبرني بالتصنيف والاحتمال لكل واحد منها: https://www.wikipedia.org https://www.python.org http://203.0.113.45/paypal.com-login/verify/account.php?session=99812 واعرض لي كل أمر نفّذته وخرجه الخام.
You can still open http://127.0.0.1:5000 in your browser and try your own domain or your university.
/health returns JSON, and the feature count in that JSON matches the list printed in step 3.5 — Deploy with no manual server work
You will not open an SSH session in this step. Claude Code runs every server command from your laptop through gcloud, and you read back what it did.
Prompt 8 — the whole server side
Deploy ~/phish-ml to my Google Cloud VM. Do all of it from here. Do not ask me to log in to the server myself. The VM is g1 in zone us-central1-a. Run remote commands as gcloud compute ssh g1 --zone=us-central1-a --command "..." and copy files with gcloud compute scp. nginx is already installed and serving on port 80. 1. Install python3-venv and python3-pip on the VM if they are missing. 2. Create /opt/phish-ml, make a virtual environment inside it, and pip install flask, gunicorn, scikit-learn, joblib and pandas into that environment. 3. Copy app.py, requirements.txt and model.joblib into /opt/phish-ml. 4. Write /etc/systemd/system/phish-ml.service so it runs the app with gunicorn from that virtual environment, with exactly one worker, bound to 127.0.0.1:8000, restarting on failure. Enable it and start it. 5. Write /etc/nginx/sites-available/phish-ml with server_name phish.yourdomain.xyz, proxy_pass to http://127.0.0.1:8000 and the usual proxy headers. Symlink it into sites-enabled, run nginx -t, and reload nginx only if the test passes. Use one worker, not more — the VM has 1 GB of RAM. Show me every command you ran and what it printed.انشر محتوى ~/phish-ml على آلتي الافتراضية في Google Cloud. نفّذ كل شيء من هنا. لا تطلب مني الدخول إلى الخادم بنفسي. الآلة هي g1 في المنطقة us-central1-a. نفّذ الأوامر البعيدة بالشكل gcloud compute ssh g1 --zone=us-central1-a --command "..." وانسخ الملفات بـ gcloud compute scp. أما nginx فهو مثبت أصلاً ويخدم على المنفذ 80. 1. ثبّت python3-venv و python3-pip على الآلة إن لم يكونا موجودين. 2. أنشئ المجلد /opt/phish-ml، وأنشئ بيئة افتراضية داخله، وثبّت فيها flask و gunicorn و scikit-learn و joblib و pandas. 3. انسخ app.py و requirements.txt و model.joblib إلى /opt/phish-ml. 4. اكتب الملف /etc/systemd/system/phish-ml.service بحيث يشغّل التطبيق بـ gunicorn من تلك البيئة الافتراضية، بعامل واحد فقط، مرتبطاً بـ 127.0.0.1:8000، ويعيد التشغيل عند الفشل. فعّله وابدأه. 5. اكتب الملف /etc/nginx/sites-available/phish-ml مع server_name phish.yourdomain.xyz، و proxy_pass إلى http://127.0.0.1:8000 مع ترويسات الوكيل المعتادة. اربطه برابط رمزي في sites-enabled، ونفّذ nginx -t، وأعد تحميل nginx فقط إذا نجح الاختبار. استخدم عاملاً واحداً لا أكثر — فالآلة فيها 1 جيجابايت من الذاكرة. واعرض لي كل أمر نفّذته وما طبعه.
Check its work: Claude Code reports success in its own words. This prompt makes it show you the raw evidence, so you can see whether it is true.
Prompt 9 — check the deployment
Check the deployment on g1 in us-central1-a. Run the server commands over gcloud compute ssh from here. Do not fix anything yet. 1. On the VM, run systemctl status phish-ml --no-pager. 2. On the VM, run sudo journalctl -u phish-ml -n 40 --no-pager. 3. From here, run curl -s http://phish.yourdomain.xyz/health. 4. From here, run curl -s http://phish.yourdomain.xyz/ and tell me whether the form HTML comes back. Show me every command you ran and its raw output.تحقّق من النشر على g1 في us-central1-a. نفّذ أوامر الخادم عبر gcloud compute ssh من هنا. ولا تصلح شيئاً بعد. 1. على الآلة، نفّذ systemctl status phish-ml --no-pager. 2. على الآلة، نفّذ sudo journalctl -u phish-ml -n 40 --no-pager. 3. من هنا، نفّذ curl -s http://phish.yourdomain.xyz/health. 4. من هنا، نفّذ curl -s http://phish.yourdomain.xyz/ وأخبرني هل تعود صفحة HTML الخاصة بالنموذج. واعرض لي كل أمر نفّذته وخرجه الخام.
active (running), JSON from /health and the form's HTML mean it worked. If the status is failed, tell Claude Code to read those logs and fix its own deployment — a missing wheel on the VM's Python version is the usual cause.
127.0.0.1:8000, which is reachable only from the VM itself. The internet only ever talks to nginx, and nginx forwards. Nothing extra needs opening in the firewall, and port 8000 stays private.active (running), and http://phish.yourdomain.xyz returns your form's HTML. You never typed a command on the server.6 — TLS and go live
One server change left. No firewall change is needed: port 443 was opened by the allow-https rule when you built the VM, and g1 already carries the https-server tag. Certbot only has to prove the name and rewrite the nginx site.
Prompt 10 — certificate and redirect
Same VM, g1 in us-central1-a, again entirely over gcloud compute ssh from here. Install certbot and its nginx plugin, then issue a Let's Encrypt certificate for phish.yourdomain.xyz using the nginx plugin, non-interactively, agreeing to the terms with my email address, and choose the option that redirects all HTTP traffic to HTTPS. Then run nginx -t, reload nginx, and show me the certificate's expiry date and that the renewal timer is active.الآلة نفسها، g1 في us-central1-a، ومرة أخرى بالكامل عبر gcloud compute ssh من هنا. ثبّت certbot وإضافته الخاصة بـ nginx، ثم أصدر شهادة Let's Encrypt للنطاق phish.yourdomain.xyz باستخدام إضافة nginx، بصيغة غير تفاعلية، مع الموافقة على الشروط بعنوان بريدي، واختر الخيار الذي يحوّل كل حركة HTTP إلى HTTPS. ثم نفّذ nginx -t، وأعد تحميل nginx، واعرض لي تاريخ انتهاء الشهادة وأن مؤقّت التجديد مفعّل.
Now check it the way a visitor would, with your own domain in place of the placeholder:
Prompt 11 — check it is live
Check that phish.yourdomain.xyz is live over HTTPS. Run everything from here. 1. Run curl -sIL http://phish.yourdomain.xyz and tell me whether the first answer is a 301 redirect to https. 2. Show me who issued the certificate for phish.yourdomain.xyz and when it expires. 3. POST https://www.wikipedia.org to https://phish.yourdomain.xyz/ the same way the form does, and tell me the verdict and the probability. Show me every command you ran and its raw output.تحقّق من أن phish.yourdomain.xyz يعمل على HTTPS. نفّذ كل شيء من هنا. 1. نفّذ curl -sIL http://phish.yourdomain.xyz وأخبرني هل الجواب الأول تحويل 301 إلى https. 2. اعرض لي من أصدر شهادة phish.yourdomain.xyz ومتى تنتهي. 3. أرسل https://www.wikipedia.org بطلب POST إلى https://phish.yourdomain.xyz/ بالطريقة نفسها التي يرسل بها النموذج، وأخبرني بالتصنيف والاحتمال. واعرض لي كل أمر نفّذته وخرجه الخام.
A 301 from http:// to https://, a Let's Encrypt certificate, and phish or benign with a probability mean you are live. That is the lab.
Optional: to see the padlock the way a visitor does, give this to Claude in Chrome (Claude in Chrome Setup):
Prompt 12 — optional: padlock check in Chrome
Open https://phish.yourdomain.xyz in a new tab. Tell me whether Chrome shows the connection as secure, and who issued the certificate. Then type https://www.wikipedia.org into the URL box, press Submit, and tell me exactly what the page shows.افتح https://phish.yourdomain.xyz في تبويب جديد. وأخبرني هل يعرض Chrome الاتصال على أنه آمن، ومن أصدر الشهادة. ثم اكتب https://www.wikipedia.org في مربع العنوان، واضغط Submit، وأخبرني بما تعرضه الصفحة بالضبط.
model.joblib out of any loop that re-uploads it.https://phish.yourdomain.xyz, a valid certificate, and a verdict for the URL it sent.Reference
| App directory on the VM | /opt/phish-ml — holds app.py, model.joblib and the virtual environment |
| systemd unit | /etc/systemd/system/phish-ml.service |
| nginx site | /etc/nginx/sites-available/phish-ml, symlinked into sites-enabled |
| gunicorn bind address | 127.0.0.1:8000, one worker — local only, nginx proxies to it |
| Service status | Ask Claude Code: “Show me the status of phish-ml on g1.” |
| Read the logs | Ask Claude Code: “Show me the last 40 lines of the phish-ml logs on g1.” |
| Restart after a new model | Ask Claude Code: “Restart phish-ml on g1 and show me its status.” |
| Test and reload nginx | Ask Claude Code: “Test the nginx config on g1 and reload nginx only if the test passes.” |
| Health check | Ask Claude Code: “Check https://phish.yourdomain.xyz/health and show me the raw JSON.” |
| Anything on the server | Ask Claude Code; it uses gcloud for you. |
Links
- UCI: PhiUSIIL Phishing URL Dataset
- scikit-learn documentation
- scikit-learn: classification metrics, including macro F1
- Flask documentation
- gunicorn
- Certbot instructions
- Namecheap: set up an A record
Current as of October 2026.