Yet Another Attempt to Restrict R in Exams (Logging Won)

teaching
exams
SEB
Author

Johannes Titz

Published

February 18, 2026

Exams at our professorship will begin soon, and I am still refining a suitable setup for using R during examinations. Our new professor would like students to have access to a full R environment—without artificial restrictions—but with comprehensive logging capabilities. After experimenting with a wide range of approaches, I have come to agree that logging is the only realistic solution. Attempting to strictly control the environment quickly proves infeasible in practice.

Basic logging can be implemented relatively easily via R’s command history. However, a more robust and flexible approach is to define a custom logging function and register it as a taskCallback.

As a first step, we set up the log file. The complete code is placed in a custom .Rprofile, and R is started via a batch script that applies several useful settings.

This is the .Rprofile:

options(help_type = "text")

histDir <- Sys.getenv("R_HISTDIR")
username <- Sys.getenv("USERNAME")

if (!dir.exists(histDir)) dir.create(histDir, recursive = TRUE)

# Create a new file at session start with timestamp
ts_session <- format(Sys.time(), "%Y%m%d-%H%M%S")
histFile <- file.path(histDir, sprintf("%s-%s.Rlog", username, ts_session))

Note that help_type must be set to text; otherwise, HTML help will be used, which attempts to open a browser window. This is not permitted during the exam and the window will be automatically closed by SEB.

Next, we obtain the working directory and the username from environment variables (which are set in the batch script). We then check whether the directory exists and create it if necessary. Finally, we initialize a session log file that includes the username and a timestamp.

Now comes the interesting part:

# Task callback: write each command with timestamp to the session file
logCommand <- function(expr, value, ok, visible) {
  entry <- paste0(Sys.time(), " | ", deparse(expr), "\n")
  cat(entry, file = histFile, append = TRUE)
  TRUE
}

if (interactive()) invisible(addTaskCallback(logCommand))

This function is registered as a callback: it is invoked after every command and appends the most recent command, together with a timestamp, to the log file (histFile).

Writing to AFS

For this setup to work smoothly, we need to be careful with AFS permissions. Granting i and l rights is obvious, but write (w) permission is also required. This makes the approach, in principle, exploitable: a user could modify the log file. However, the practical incentive to do so is limited. Since the file is not readable, selectively removing or altering specific lines would be guesswork. Deleting the entire file or replacing it with arbitrary commands would be possible, but such behavior would be highly conspicuous—especially if the content no longer corresponds to the actual exam tasks.

A more secure variant using only i and l rights is also possible. In that case, a separate file would have to be created for each command. While technically feasible, this approach quickly becomes messy. For now, we therefore opt for the simpler solution; if it turns out to be insufficiently secure, we can always switch to the stricter alternative later.

One might assume that it is easy to disable or manipulate the command history from within R. This is true in principle, but the very first command used to turn off or alter the history will itself be written to the log. With i and l AFS permissions, this entry cannot be removed. With modern AI-based tools, detecting such anomalous commands automatically is straightforward. Of course, with w permissions this becomes a different game—but, as noted, we can always revert to the more restrictive setup if necessary.

Finally, it is worth noting that users could set help_type to html in an attempt to open a browser (or simply browseURL). SEB prevents this and such attempts are still visible in the logs.